UNPKG

@gebrai/gebrai

Version:

Model Context Protocol server for GeoGebra mathematical visualization

1,178 lines (1,167 loc) 58.4 kB
"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