@gebrai/gebrai
Version:
Model Context Protocol server for GeoGebra mathematical visualization
1,178 lines (1,167 loc) • 58.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeoGebraInstance = void 0;
// @ts-nocheck
const puppeteer_1 = __importDefault(require("puppeteer"));
const geogebra_1 = require("../types/geogebra");
const logger_1 = __importDefault(require("./logger"));
const uuid_1 = require("uuid");
/**
* GeoGebra Instance - Manages a single GeoGebra instance via Puppeteer
*/
/**
* Escapes a string for safe use in JavaScript code.
* Replaces backslashes and single quotes with their escaped equivalents.
*/
function escapeForJavaScript(input) {
return input.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
class GeoGebraInstance {
browser;
page;
isInitialized = false;
lastActivity = new Date();
id;
config;
constructor(config = {}) {
this.id = (0, uuid_1.v4)();
this.config = {
appName: 'classic', // Changed from 'graphing' to 'classic' for full functionality
width: 800,
height: 600,
showMenuBar: false,
showToolBar: false,
showAlgebraInput: false,
showResetIcon: false,
enableRightClick: true,
language: 'en',
...config
};
logger_1.default.info(`Created GeoGebra instance ${this.id}`, { config: this.config });
}
/**
* Initialize the GeoGebra instance
*/
async initialize(headless = true, browserArgs = []) {
try {
logger_1.default.info(`Initializing GeoGebra instance ${this.id}`, { headless });
// Launch browser
this.browser = await puppeteer_1.default.launch({
headless,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
...browserArgs
]
});
// Create new page
this.page = await this.browser.newPage();
// Set viewport
await this.page.setViewport({
width: this.config.width || 800,
height: this.config.height || 600
});
// Load GeoGebra
await this.loadGeoGebra();
// Wait for GeoGebra to be ready
await this.waitForReady();
this.isInitialized = true;
this.updateActivity();
logger_1.default.info(`GeoGebra instance ${this.id} initialized successfully`);
}
catch (error) {
logger_1.default.error(`Failed to initialize GeoGebra instance ${this.id}`, error);
await this.cleanup();
throw new geogebra_1.GeoGebraConnectionError(`Failed to initialize GeoGebra: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Load GeoGebra applet in the browser
*/
async loadGeoGebra() {
if (!this.page) {
throw new geogebra_1.GeoGebraConnectionError('Page not initialized');
}
const appletHTML = this.generateAppletHTML();
await this.page.setContent(appletHTML);
// Wait for GeoGebra to load
await this.page.waitForFunction('window.ggbApplet', { timeout: 30000 });
}
/**
* Generate HTML content with GeoGebra applet
*/
generateAppletHTML() {
const config = this.config;
return `
<!DOCTYPE html>
<html>
<head>
<title>GeoGebra Applet</title>
<script src="https://www.geogebra.org/apps/deployggb.js"></script>
</head>
<body>
<div id="ggb-element"></div>
<script>
// Initialize global variables
window.ggbReady = false;
window.ggbApplet = null;
// GeoGebra initialization callback
window.ggbOnInit = function(name) {
window.ggbApplet = window[name];
window.ggbReady = true;
console.log('GeoGebra initialized successfully with applet:', name);
};
const parameters = {
"appName": "${config.appName}",
"width": ${config.width},
"height": ${config.height},
"showMenuBar": ${config.showMenuBar},
"showToolBar": ${config.showToolBar},
"showAlgebraInput": ${config.showAlgebraInput},
"showResetIcon": ${config.showResetIcon},
"enableRightClick": ${config.enableRightClick},
"enableLabelDrags": ${config.enableLabelDrags || true},
"enableShiftDragZoom": ${config.enableShiftDragZoom || true},
"enableCAS": true,
"enable3D": false,
"language": "${config.language}",
${config.material_id ? `"material_id": "${config.material_id}",` : ''}
${config.filename ? `"filename": "${config.filename}",` : ''}
${config.ggbBase64 ? `"ggbBase64": "${config.ggbBase64}",` : ''}
"useBrowserForJS": false,
"preventFocus": true,
"appletOnLoad": function(api) {
window.ggbApplet = api;
// Ensure CAS is loaded
if (api.enableCAS) {
api.enableCAS(true);
}
// Load all required modules with retries
var loadAttempts = 0;
var maxAttempts = 10;
function checkModulesLoaded() {
loadAttempts++;
// Test CAS availability
var casReady = false;
try {
api.evalCommand('1+1');
casReady = true;
} catch (e) {
casReady = false;
}
// Test scripting availability
var scriptingReady = false;
try {
// Try a simple scripting command
var result = api.evalCommand('test_slider = Slider(0, 1)');
if (result) {
api.deleteObject('test_slider');
scriptingReady = true;
}
} catch (e) {
scriptingReady = false;
}
if (casReady && scriptingReady) {
window.ggbReady = true;
console.log('GeoGebra applet loaded with all modules ready');
} else if (loadAttempts < maxAttempts) {
setTimeout(checkModulesLoaded, 500);
} else {
// Fallback - mark as ready even if modules aren't fully loaded
window.ggbReady = true;
console.log('GeoGebra applet loaded with basic functionality (modules may still be loading)');
}
}
// Start checking after initial delay
setTimeout(checkModulesLoaded, 1000);
}
};
// Create and inject the applet
const applet = new GGBApplet(parameters, true);
applet.inject('ggb-element');
// Fallback timeout to set ready state
setTimeout(function() {
if (!window.ggbReady && window.ggbApplet) {
window.ggbReady = true;
console.log('GeoGebra initialized via fallback timeout');
}
}, 5000);
</script>
</body>
</html>`;
}
/**
* Wait for GeoGebra to be ready
*/
async waitForReady(timeout = 30000) {
if (!this.page) {
throw new geogebra_1.GeoGebraConnectionError('Page not initialized');
}
try {
// Wait for both ggbReady flag and ggbApplet to be available
await this.page.waitForFunction('window.ggbReady === true && window.ggbApplet && typeof window.ggbApplet.evalCommand === "function"', { timeout });
// Additional verification that the applet is functional
const isWorking = await this.page.evaluate(() => {
try {
return typeof window.ggbApplet.evalCommand === 'function' &&
typeof window.ggbApplet.exists === 'function';
}
catch (e) {
return false;
}
});
if (!isWorking) {
throw new Error('GeoGebra applet methods not available');
}
logger_1.default.debug(`GeoGebra instance ${this.id} is ready and functional`);
}
catch (error) {
// Log the current state for debugging
try {
const debugInfo = await this.page.evaluate(() => ({
ggbReady: window.ggbReady,
ggbAppletExists: !!window.ggbApplet,
ggbAppletType: typeof window.ggbApplet,
evalCommandExists: window.ggbApplet && typeof window.ggbApplet.evalCommand,
pageLocation: window.location.href,
pageTitle: document.title
}));
logger_1.default.error(`GeoGebra initialization failed. Debug info:`, debugInfo);
}
catch (debugError) {
logger_1.default.error(`Failed to get debug info during initialization failure`, debugError);
}
throw new geogebra_1.GeoGebraConnectionError('GeoGebra failed to initialize within timeout');
}
}
/**
* Execute a GeoGebra command
*/
async evalCommand(command) {
this.ensureInitialized();
this.updateActivity();
try {
logger_1.default.debug(`Executing command on instance ${this.id}: ${command}`);
// First verify that the applet is still available
const appletCheck = await this.page.evaluate(() => {
return {
appletExists: !!window.ggbApplet,
evalCommandExists: !!window.ggbApplet && typeof window.ggbApplet.evalCommand === 'function'
};
});
if (!appletCheck.appletExists || !appletCheck.evalCommandExists) {
throw new Error(`GeoGebra applet not available: ${JSON.stringify(appletCheck)}`);
}
const result = await this.page.evaluate((cmd) => {
try {
const success = window.ggbApplet.evalCommand(cmd);
return {
success: success,
error: success ? undefined : 'Command execution failed'
};
}
catch (error) {
return {
success: false,
error: error.message || 'Unknown error'
};
}
}, command);
if (!result.success) {
throw new geogebra_1.GeoGebraCommandError(result.error || 'Command failed', command);
}
logger_1.default.debug(`Command executed successfully on instance ${this.id}: ${command}`);
return result;
}
catch (error) {
logger_1.default.error(`Command execution failed on instance ${this.id}`, { command, error });
if (error instanceof geogebra_1.GeoGebraCommandError) {
throw error;
}
throw new geogebra_1.GeoGebraCommandError(`Failed to execute command: ${error instanceof Error ? error.message : String(error)}`, command);
}
}
/**
* Execute command and get labels of created objects
*/
async evalCommandGetLabels(command) {
this.ensureInitialized();
this.updateActivity();
try {
const script = [
'(function(cmd) {',
' const result = window.ggbApplet.evalCommandGetLabels(cmd);',
' return result ? result.split(",").filter(function(label) { return label.trim(); }) : [];',
`})('${command.replace(/'/g, "\\'")}');`
].join('\n');
const labels = await this.page.evaluate(script);
logger_1.default.debug(`Command executed on instance ${this.id}, labels: ${labels.join(', ')}`);
return labels;
}
catch (error) {
logger_1.default.error(`Failed to execute command with labels on instance ${this.id}`, { command, error });
throw new geogebra_1.GeoGebraCommandError(`Failed to execute command: ${error instanceof Error ? error.message : String(error)}`, command);
}
}
/**
* Delete an object
*/
async deleteObject(objName) {
this.ensureInitialized();
this.updateActivity();
try {
await this.page.evaluate(`window.ggbApplet.deleteObject('${escapeForJavaScript(objName)}');`);
return true;
}
catch (error) {
logger_1.default.error(`Failed to delete object ${objName} on instance ${this.id}`, error);
return false;
}
}
/**
* Check if object exists
*/
async exists(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const exists = await this.page.evaluate((name) => {
return window.ggbApplet.exists(name);
}, objName);
return exists;
}
catch (error) {
logger_1.default.error(`Failed to check existence of object ${objName} on instance ${this.id}`, error);
return false;
}
}
/**
* Check if object is defined
*/
async isDefined(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const defined = await this.page.evaluate((name) => {
return window.ggbApplet.isDefined(name);
}, objName);
return defined;
}
catch (error) {
logger_1.default.error(`Failed to check if object ${objName} is defined on instance ${this.id}`, error);
return false;
}
}
/**
* Get all object names
*/
async getAllObjectNames(type) {
this.ensureInitialized();
this.updateActivity();
try {
const names = await this.page.evaluate((objType) => {
return window.ggbApplet.getAllObjectNames(objType);
}, type);
return names || [];
}
catch (error) {
logger_1.default.error(`Failed to get object names on instance ${this.id}`, error);
return [];
}
}
/**
* Get object information
*/
async getObjectInfo(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const info = await this.page.evaluate((name) => {
if (!window.ggbApplet.exists(name)) {
return null;
}
const type = window.ggbApplet.getObjectType(name);
// Base information that all objects have
const baseInfo = {
name,
type,
visible: window.ggbApplet.getVisible(name),
defined: window.ggbApplet.isDefined(name),
color: window.ggbApplet.getColor(name)
};
// Try to get value and valueString safely
let value = null;
let valueString = null;
try {
value = window.ggbApplet.getValue(name);
}
catch (e) {
// Some objects don't have a numeric value
}
try {
valueString = window.ggbApplet.getValueString(name);
}
catch (e) {
// Some objects don't have a value string
}
// Only try to get coordinates for objects that support them
// Points, segments, lines, circles typically have coordinates
// Functions, curves, implicit curves typically don't
const coordinateTypes = ['point', 'segment', 'line', 'circle', 'polygon', 'vector'];
let coordinates = {};
if (coordinateTypes.includes(type.toLowerCase())) {
try {
coordinates = {
x: window.ggbApplet.getXcoord(name),
y: window.ggbApplet.getYcoord(name),
z: window.ggbApplet.getZcoord(name)
};
}
catch (e) {
// If coordinate access fails, just omit coordinates
}
}
return {
...baseInfo,
value,
valueString,
...coordinates
};
}, objName);
return info;
}
catch (error) {
logger_1.default.error(`Failed to get object info for ${objName} on instance ${this.id}`, error);
return null;
}
}
/**
* Get X coordinate of object
*/
async getXcoord(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const x = await this.page.evaluate((name) => {
return window.ggbApplet.getXcoord(name);
}, objName);
return x || 0;
}
catch (error) {
logger_1.default.error(`Failed to get X coordinate of ${objName} on instance ${this.id}`, error);
return 0;
}
}
/**
* Get Y coordinate of object
*/
async getYcoord(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const y = await this.page.evaluate((name) => {
return window.ggbApplet.getYcoord(name);
}, objName);
return y || 0;
}
catch (error) {
logger_1.default.error(`Failed to get Y coordinate of ${objName} on instance ${this.id}`, error);
return 0;
}
}
/**
* Get Z coordinate of object
*/
async getZcoord(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const z = await this.page.evaluate((name) => {
return window.ggbApplet.getZcoord(name);
}, objName);
return z || 0;
}
catch (error) {
logger_1.default.error(`Failed to get Z coordinate of ${objName} on instance ${this.id}`, error);
return 0;
}
}
/**
* Get value of object
*/
async getValue(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const value = await this.page.evaluate((name) => {
return window.ggbApplet.getValue(name);
}, objName);
return value || 0;
}
catch (error) {
logger_1.default.error(`Failed to get value of ${objName} on instance ${this.id}`, error);
return 0;
}
}
/**
* Get value string of object
*/
async getValueString(objName) {
this.ensureInitialized();
this.updateActivity();
try {
const valueStr = await this.page.evaluate((name) => {
return window.ggbApplet.getValueString(name);
}, objName);
return valueStr || '';
}
catch (error) {
logger_1.default.error(`Failed to get value string of ${objName} on instance ${this.id}`, error);
return '';
}
}
/**
* Clear construction
*/
async newConstruction() {
this.ensureInitialized();
this.updateActivity();
try {
await this.page.evaluate(() => {
window.ggbApplet.newConstruction();
});
logger_1.default.debug(`Construction cleared on instance ${this.id}`);
}
catch (error) {
logger_1.default.error(`Failed to clear construction on instance ${this.id}`, error);
throw new geogebra_1.GeoGebraError(`Failed to clear construction: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Reset construction
*/
async reset() {
this.ensureInitialized();
this.updateActivity();
try {
await this.page.evaluate(() => {
window.ggbApplet.reset();
});
logger_1.default.debug(`Construction reset on instance ${this.id}`);
}
catch (error) {
logger_1.default.error(`Failed to reset construction on instance ${this.id}`, error);
throw new geogebra_1.GeoGebraError(`Failed to reset construction: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Refresh views
*/
async refreshViews() {
this.ensureInitialized();
this.updateActivity();
try {
await this.page.evaluate(() => {
window.ggbApplet.refreshViews();
});
logger_1.default.debug(`Views refreshed on instance ${this.id}`);
}
catch (error) {
logger_1.default.error(`Failed to refresh views on instance ${this.id}`, error);
throw new geogebra_1.GeoGebraError(`Failed to refresh views: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Set coordinate system bounds
*/
async setCoordSystem(xmin, xmax, ymin, ymax) {
this.ensureInitialized();
this.updateActivity();
try {
await this.page.evaluate((xmin, xmax, ymin, ymax) => {
window.ggbApplet.setCoordSystem(xmin, xmax, ymin, ymax);
}, xmin, xmax, ymin, ymax);
logger_1.default.debug(`Coordinate system set on instance ${this.id}: x[${xmin}, ${xmax}], y[${ymin}, ${ymax}]`);
}
catch (error) {
logger_1.default.error(`Failed to set coordinate system on instance ${this.id}`, error);
throw new geogebra_1.GeoGebraError(`Failed to set coordinate system: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Set axes visibility
*/
async setAxesVisible(xAxis, yAxis) {
this.ensureInitialized();
this.updateActivity();
try {
await this.page.evaluate((xAxis, yAxis) => {
window.ggbApplet.setAxesVisible(xAxis, yAxis);
}, xAxis, yAxis);
logger_1.default.debug(`Axes visibility set on instance ${this.id}: x=${xAxis}, y=${yAxis}`);
}
catch (error) {
logger_1.default.error(`Failed to set axes visibility on instance ${this.id}`, error);
throw new geogebra_1.GeoGebraError(`Failed to set axes visibility: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Set grid visibility
*/
async setGridVisible(visible) {
this.ensureInitialized();
this.updateActivity();
try {
await this.page.evaluate((visible) => {
window.ggbApplet.setGridVisible(visible);
}, visible);
logger_1.default.debug(`Grid visibility set on instance ${this.id}: ${visible}`);
}
catch (error) {
logger_1.default.error(`Failed to set grid visibility on instance ${this.id}`, error);
throw new geogebra_1.GeoGebraError(`Failed to set grid visibility: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Check if GeoGebra is ready
*/
async isReady() {
if (!this.page)
return false;
try {
const ready = await this.page.evaluate(() => {
return window.ggbReady === true && window.ggbApplet;
});
return ready;
}
catch (error) {
logger_1.default.error(`Failed to check ready state on instance ${this.id}`, error);
return false;
}
}
/**
* Export construction as PNG (base64) with enhanced parameters
* GEB-17: Enhanced with validation, retry logic, and better error handling
*/
async exportPNG(scale = 1, transparent = false, dpi = 72, width, height) {
this.ensureInitialized();
this.updateActivity();
// GEB-17: Validate export readiness before attempting
const readiness = await this.validateExportReadiness('png');
if (!readiness.ready) {
throw new geogebra_1.GeoGebraError(`PNG export not ready: ${readiness.issues.join(', ')}. Recommendations: ${readiness.recommendations.join(', ')}`);
}
return this.retryOperation(async () => {
const pngBase64 = await this.page.evaluate((scale, transparent, dpi, width, height) => {
try {
// GEB-17: Enhanced applet availability check
const applet = window.ggbApplet;
if (!applet) {
throw new Error('GeoGebra applet not available');
}
if (typeof applet.getPNGBase64 !== 'function') {
throw new Error('getPNGBase64 method not available on applet');
}
// Calculate effective scale if width/height provided
let effectiveScale = scale;
if (width !== undefined && height !== undefined) {
// Get current dimensions with better error handling
let graphics;
try {
graphics = applet.getGraphicsOptions ? applet.getGraphicsOptions() : { width: 800, height: 600 };
}
catch (e) {
// Fallback to default dimensions if getGraphicsOptions fails
graphics = { width: 800, height: 600 };
}
const currentWidth = graphics.width || 800;
const currentHeight = graphics.height || 600;
const scaleX = width / currentWidth;
const scaleY = height / currentHeight;
effectiveScale = Math.min(scaleX, scaleY);
}
// GEB-17: Enhanced PNG export with better error handling and validation
let result = null;
let lastError = null;
// Try primary method with full parameters
try {
result = applet.getPNGBase64(effectiveScale, transparent, dpi);
if (result && typeof result === 'string' && result.length > 0) {
return result; // Success with primary method
}
}
catch (e1) {
lastError = e1;
// Continue to fallback methods
}
// Try without dpi parameter
try {
result = applet.getPNGBase64(effectiveScale, transparent);
if (result && typeof result === 'string' && result.length > 0) {
return result; // Success with fallback method
}
}
catch (e2) {
lastError = e2;
// Continue to next fallback
}
// Try simplest approach with just scale
try {
result = applet.getPNGBase64(effectiveScale);
if (result && typeof result === 'string' && result.length > 0) {
return result; // Success with simple method
}
}
catch (e3) {
lastError = e3;
// Continue to final fallback
}
// Try with default scale as last resort
try {
result = applet.getPNGBase64(1);
if (result && typeof result === 'string' && result.length > 0) {
return result; // Success with default scale
}
}
catch (e4) {
lastError = e4;
}
// If we get here, all methods failed
throw new Error(`All PNG export methods failed. Last error: ${lastError ? lastError.message : 'Unknown error'}. Result was: ${typeof result} with length ${result ? result.length : 'undefined'}`);
}
catch (error) {
throw new Error(`PNG export failed: ${error.message || error}`);
}
}, scale, transparent, dpi, width, height);
// GEB-17: Enhanced validation of the result
if (!pngBase64) {
throw new Error('PNG export returned null or undefined');
}
if (typeof pngBase64 !== 'string') {
throw new Error(`PNG export returned invalid type: ${typeof pngBase64}`);
}
if (pngBase64.length === 0) {
throw new Error('PNG export returned empty string');
}
// Enhanced base64 validation
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(pngBase64)) {
throw new Error(`Result is not valid base64. Length: ${pngBase64.length}, starts with: ${pngBase64.substring(0, 50)}`);
}
// Additional check for minimum reasonable size (base64 should be substantial for a real image)
if (pngBase64.length < 100) {
throw new Error(`PNG result suspiciously small (${pngBase64.length} characters): ${pngBase64}`);
}
logger_1.default.debug(`PNG exported from instance ${this.id} with scale ${scale}, transparent ${transparent}, dpi ${dpi}, dimensions ${width}x${height}, result length: ${pngBase64.length}`);
return pngBase64;
}, 3, 1000, 'PNG export');
}
/**
* Export construction as SVG
* GEB-17: Enhanced with better validation and error handling, no more placeholder responses
*/
async exportSVG() {
this.ensureInitialized();
this.updateActivity();
// GEB-17: Validate export readiness before attempting
const readiness = await this.validateExportReadiness('svg');
if (!readiness.ready) {
throw new geogebra_1.GeoGebraError(`SVG export not ready: ${readiness.issues.join(', ')}. Recommendations: ${readiness.recommendations.join(', ')}`);
}
return this.retryOperation(async () => {
const svg = await this.page.evaluate(() => {
try {
// GEB-17: Enhanced applet availability check
const applet = window.ggbApplet;
if (!applet) {
throw new Error('GeoGebra applet not available');
}
let result = null;
let lastError = null;
// Try exportSVG method first
if (typeof applet.exportSVG === 'function') {
try {
result = applet.exportSVG();
if (result && typeof result === 'string' && result.includes('<svg')) {
return result; // Success with exportSVG
}
}
catch (e1) {
lastError = e1;
}
// Try with filename parameter
try {
result = applet.exportSVG('construction');
if (result && typeof result === 'string' && result.includes('<svg')) {
return result; // Success with filename parameter
}
}
catch (e2) {
lastError = e2;
}
}
// Try getSVG method as alternative
if (typeof applet.getSVG === 'function') {
try {
result = applet.getSVG();
if (result && typeof result === 'string' && result.includes('<svg')) {
return result; // Success with getSVG
}
}
catch (e3) {
lastError = e3;
}
}
// GEB-17: NO MORE PLACEHOLDER RESPONSES - throw error instead
const availableMethods = [];
if (typeof applet.exportSVG === 'function')
availableMethods.push('exportSVG');
if (typeof applet.getSVG === 'function')
availableMethods.push('getSVG');
throw new Error(`All SVG export methods failed. Available methods: ${availableMethods.join(', ')}. Last error: ${lastError ? lastError.message : 'Unknown error'}. Result was: ${typeof result} ${result ? (result.includes ? (result.includes('<svg') ? 'contains <svg>' : 'missing <svg>') : 'not string-like') : 'null/undefined'}`);
}
catch (error) {
throw new Error(`SVG export failed: ${error.message || error}`);
}
});
// GEB-17: Enhanced validation - no placeholder acceptance
if (!svg) {
throw new Error('SVG export returned null or undefined');
}
if (typeof svg !== 'string') {
throw new Error(`SVG export returned invalid type: ${typeof svg}`);
}
if (svg.length === 0) {
throw new Error('SVG export returned empty string');
}
// Strict SVG validation - must be actual SVG, not placeholder
if (!svg.includes('<svg')) {
throw new Error(`Result does not contain valid SVG content. Length: ${svg.length}, starts with: ${svg.substring(0, 100)}`);
}
// Check for our old placeholder pattern and reject it
if (svg.includes('SVG export not available')) {
throw new Error('SVG export returned placeholder content instead of actual SVG');
}
// Additional validation for minimum reasonable SVG size
if (svg.length < 50) {
throw new Error(`SVG result suspiciously small (${svg.length} characters): ${svg}`);
}
logger_1.default.debug(`SVG exported from instance ${this.id}, result length: ${svg.length}`);
return svg;
}, 3, 1000, 'SVG export');
}
/**
* Export construction as PDF (base64)
* GEB-17: Enhanced with better connection management and timeout handling
*/
async exportPDF() {
this.ensureInitialized();
this.updateActivity();
// GEB-17: Validate export readiness before attempting
const readiness = await this.validateExportReadiness('pdf');
if (!readiness.ready) {
throw new geogebra_1.GeoGebraError(`PDF export not ready: ${readiness.issues.join(', ')}. Recommendations: ${readiness.recommendations.join(', ')}`);
}
return this.retryOperation(async () => {
// GEB-17: Enhanced page availability check
if (!this.page || this.page.isClosed()) {
throw new Error('Browser page not available for PDF generation');
}
try {
// GEB-17: Set longer timeout for PDF generation to prevent socket hang up
const pdfOptions = {
format: 'A4',
printBackground: true,
margin: {
top: '0.5in',
bottom: '0.5in',
left: '0.5in',
right: '0.5in'
},
timeout: 30000 // 30 second timeout to prevent socket hang up
};
logger_1.default.debug(`Starting PDF generation for instance ${this.id}`);
// GEB-17: Wait for page to be fully loaded and stable before PDF generation
await this.page.waitForLoadState?.('domcontentloaded', { timeout: 10000 }).catch(() => {
// Ignore if waitForLoadState is not available or times out
logger_1.default.debug('waitForLoadState not available or timed out, proceeding with PDF generation');
});
// Generate PDF with enhanced error handling
const pdf = await this.page.pdf(pdfOptions);
if (!pdf || pdf.length === 0) {
throw new Error('PDF generation returned empty or null result');
}
const pdfBase64 = pdf.toString('base64');
// GEB-17: Validate the PDF result
if (!pdfBase64 || typeof pdfBase64 !== 'string') {
throw new Error('PDF to base64 conversion failed');
}
if (pdfBase64.length === 0) {
throw new Error('PDF base64 result is empty');
}
// Basic validation that it's valid base64
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(pdfBase64)) {
throw new Error(`PDF result is not valid base64. Length: ${pdfBase64.length}`);
}
// Check for minimum reasonable PDF size (PDFs should be substantial)
if (pdfBase64.length < 500) {
throw new Error(`PDF result suspiciously small (${pdfBase64.length} characters)`);
}
logger_1.default.debug(`PDF exported from instance ${this.id}, result length: ${pdfBase64.length}`);
return pdfBase64;
}
catch (error) {
// GEB-17: Enhanced error handling for specific PDF generation issues
if (error.message && error.message.includes('timeout')) {
throw new Error(`PDF generation timeout: ${error.message}`);
}
if (error.message && error.message.includes('socket hang up')) {
throw new Error(`PDF generation connection error (socket hang up): ${error.message}`);
}
if (error.message && error.message.includes('disconnected')) {
throw new Error(`Browser disconnected during PDF generation: ${error.message}`);
}
throw new Error(`PDF generation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}, 3, 2000, 'PDF export'); // Longer delay between retries for PDF
}
/**
* Export animation as GIF or video frames
* GEB-17: Enhanced with comprehensive applet availability checks
*/
async exportAnimation(options = {}) {
this.ensureInitialized();
this.updateActivity();
// GEB-17: Validate export readiness before attempting
const readiness = await this.validateExportReadiness('animation');
if (!readiness.ready) {
throw new geogebra_1.GeoGebraError(`Animation export not ready: ${readiness.issues.join(', ')}. Recommendations: ${readiness.recommendations.join(', ')}`);
}
const { duration = 5000, // 5 seconds
frameRate = 10, // 10 fps
format = 'frames', width, height } = options;
return this.retryOperation(async () => {
// GEB-17: Enhanced applet availability and animation readiness check
const animationCheck = await this.page.evaluate(() => {
const applet = window.ggbApplet;
if (!applet) {
return { ready: false, error: 'GeoGebra applet not available' };
}
if (typeof applet.startAnimation !== 'function' || typeof applet.stopAnimation !== 'function') {
return { ready: false, error: 'Animation methods not available on applet' };
}
if (typeof applet.isAnimationRunning !== 'function') {
return { ready: false, error: 'Animation status method not available' };
}
// Check if there are any objects that can be animated
try {
const allObjects = applet.getAllObjectNames();
if (!allObjects || allObjects.length === 0) {
return { ready: false, error: 'No objects available for animation' };
}
}
catch (e) {
return { ready: false, error: 'Cannot access object list for animation' };
}
return { ready: true, error: null };
});
if (!animationCheck.ready) {
throw new Error(`Animation export failed: ${animationCheck.error}`);
}
const frameInterval = 1000 / frameRate;
const totalFrames = Math.ceil(duration / frameInterval);
const frames = [];
logger_1.default.debug(`Starting animation capture for instance ${this.id}: ${totalFrames} frames over ${duration}ms`);
try {
// Start animation
await this.page.evaluate(() => {
window.ggbApplet.startAnimation();
});
// Capture frames
for (let i = 0; i < totalFrames; i++) {
// Wait for frame interval
await new Promise(resolve => setTimeout(resolve, frameInterval));
// Capture frame as PNG
try {
const frameData = await this.exportPNG(1, false, 72, width, height);
frames.push(frameData);
logger_1.default.debug(`Captured frame ${i + 1}/${totalFrames} for instance ${this.id}`);
}
catch (frameError) {
logger_1.default.warn(`Failed to capture frame ${i + 1}/${totalFrames} for instance ${this.id}:`, frameError);
// Continue with next frame rather than failing completely
}
}
// Stop animation
await this.page.evaluate(() => {
window.ggbApplet.stopAnimation();
});
if (frames.length === 0) {
throw new Error('No frames were successfully captured');
}
logger_1.default.debug(`Animation capture completed for instance ${this.id}: ${frames.length} frames captured`);
if (format === 'frames') {
return frames;
}
else {
// For GIF format, we would need additional processing
// For now, return the frames and let the caller handle GIF creation
logger_1.default.warn('GIF format not yet implemented, returning frames');
return frames;
}
}
catch (error) {
// Ensure animation is stopped even if capture fails
try {
await this.page.evaluate(() => {
window.ggbApplet.stopAnimation();
});
}
catch (stopError) {
logger_1.default.error(`Failed to stop animation after capture error:`, stopError);
}
throw error;
}
}, 2, 3000, 'Animation export'); // Fewer retries, longer delay for animation
}
/**
* Cleanup resources
*/
async cleanup() {
logger_1.default.debug(`Cleaning up GeoGebra instance ${this.id}`);
try {
if (this.page && !this.page.isClosed()) {
await this.page.close();
}
}
catch (error) {
logger_1.default.error(`Error closing page for instance ${this.id}`, error);
}
try {
if (this.browser) {
await this.browser.close();
}
}
catch (error) {
logger_1.default.error(`Error closing browser for instance ${this.id}`, error);
}
this.page = undefined;
this.browser = undefined;
this.isInitialized = false;
logger_1.default.debug(`GeoGebra instance ${this.id} cleaned up successfully`);
}
/**
* Get instance state
*/
getState() {
return {
id: this.id,
isReady: this.isInitialized,
lastActivity: this.lastActivity,
config: this.config
};
}
/**
* GEB-17: Enhanced diagnostic method to check applet health
*/
async checkAppletHealth() {
this.ensureInitialized();
try {
const healthCheck = await this.page.evaluate(() => {
const issues = [];
const applet = window.ggbApplet;
// Check basic applet availability
const hasApplet = !!applet;
if (!hasApplet) {
issues.push('GeoGebra applet not found');
}
// Check core methods
const hasEvalCommand = hasApplet && typeof applet.evalCommand === 'function';
if (!hasEvalCommand) {
issues.push('evalCommand method not available');
}
// Check export methods
const hasPNGExport = hasApplet && typeof applet.getPNGBase64 === 'function';
const hasSVGExport = hasApplet && (typeof applet.exportSVG === 'function' || typeof applet.getSVG === 'function');
if (!hasPNGExport) {
issues.push('PNG export method (getPNGBase64) not available');
}
if (!hasSVGExport) {
issues.push('SVG export methods not available');
}
// Check animation methods
const hasAnimationMethods = hasApplet &&
typeof applet.startAnimation === 'function' &&
typeof applet.stopAnimation === 'function';
if (!hasAnimationMethods) {
issues.push('Animation methods not available');
}
// Test a simple command to verify functionality
let commandWorks = false;
if (hasEvalCommand) {
try {
// Try a harmless command
const result = applet.evalCommand('1+1');
commandWorks = true;
}
catch (e) {
issues.push('Command execution test failed: ' + e.message);
}
}
return {
issues,
capabilities: {
hasApplet,
hasEvalCommand: hasEvalCommand && commandWorks,
hasExportMethods: hasPNGExport && hasSVGExport,
hasAnimationMethods
},
exportAvailability: {
png: hasPNGExport,
svg: hasSVGExport,
pdf: true // PDF uses page.pdf(), not applet method
}
};
});
const isHealthy = healthCheck.issues.length === 0;
return {
isHealthy,
...healthCheck
};
}
catch (error) {
return {
isHealthy: false,
issues: [`Health check failed: ${error instanceof Error ? error.message : String(error)}`],
capabilities: {
hasApplet: false,
hasEvalCommand: false,
hasExportMethods: false,
hasAnimationMethods: false
},
exportAvailability: {
png: false,
svg: false,
pdf: false
}
};
}
}
/**
* GEB-17: Enhanced method to validate export readiness before attempting export
*/
async validateExportReadiness(exportType) {
this.ensureInitialized();
const health = await this.checkAppletHealth();
const issues = [];
const recommendations = [];
if (!health.isHealthy) {
issues.push(...health.issues);
recommendations.push('Reinitialize t