@teachinglab/omd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +138 -0
  2. package/canvas/core/canvasConfig.js +203 -0
  3. package/canvas/core/omdCanvas.js +475 -0
  4. package/canvas/drawing/segment.js +168 -0
  5. package/canvas/drawing/stroke.js +386 -0
  6. package/canvas/events/eventManager.js +435 -0
  7. package/canvas/events/pointerEventHandler.js +263 -0
  8. package/canvas/features/focusFrameManager.js +287 -0
  9. package/canvas/index.js +49 -0
  10. package/canvas/tools/eraserTool.js +322 -0
  11. package/canvas/tools/pencilTool.js +319 -0
  12. package/canvas/tools/selectTool.js +457 -0
  13. package/canvas/tools/tool.js +223 -0
  14. package/canvas/tools/toolManager.js +394 -0
  15. package/canvas/ui/cursor.js +438 -0
  16. package/canvas/ui/toolbar.js +304 -0
  17. package/canvas/utils/boundingBox.js +378 -0
  18. package/canvas/utils/mathUtils.js +259 -0
  19. package/docs/api/configuration-options.md +104 -0
  20. package/docs/api/eventManager.md +68 -0
  21. package/docs/api/focusFrameManager.md +150 -0
  22. package/docs/api/index.md +91 -0
  23. package/docs/api/main.md +58 -0
  24. package/docs/api/omdBinaryExpressionNode.md +227 -0
  25. package/docs/api/omdCanvas.md +142 -0
  26. package/docs/api/omdConfigManager.md +192 -0
  27. package/docs/api/omdConstantNode.md +117 -0
  28. package/docs/api/omdDisplay.md +121 -0
  29. package/docs/api/omdEquationNode.md +161 -0
  30. package/docs/api/omdEquationSequenceNode.md +301 -0
  31. package/docs/api/omdEquationStack.md +139 -0
  32. package/docs/api/omdFunctionNode.md +141 -0
  33. package/docs/api/omdGroupNode.md +182 -0
  34. package/docs/api/omdHelpers.md +96 -0
  35. package/docs/api/omdLeafNode.md +163 -0
  36. package/docs/api/omdNode.md +101 -0
  37. package/docs/api/omdOperationDisplayNode.md +139 -0
  38. package/docs/api/omdOperatorNode.md +127 -0
  39. package/docs/api/omdParenthesisNode.md +122 -0
  40. package/docs/api/omdPopup.md +117 -0
  41. package/docs/api/omdPowerNode.md +127 -0
  42. package/docs/api/omdRationalNode.md +128 -0
  43. package/docs/api/omdSequenceNode.md +128 -0
  44. package/docs/api/omdSimplification.md +110 -0
  45. package/docs/api/omdSqrtNode.md +79 -0
  46. package/docs/api/omdStepVisualizer.md +115 -0
  47. package/docs/api/omdStepVisualizerHighlighting.md +61 -0
  48. package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
  49. package/docs/api/omdStepVisualizerLayout.md +60 -0
  50. package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
  51. package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
  52. package/docs/api/omdToolbar.md +102 -0
  53. package/docs/api/omdTranscriptionService.md +76 -0
  54. package/docs/api/omdTreeDiff.md +134 -0
  55. package/docs/api/omdUnaryExpressionNode.md +174 -0
  56. package/docs/api/omdUtilities.md +70 -0
  57. package/docs/api/omdVariableNode.md +148 -0
  58. package/docs/api/selectTool.md +74 -0
  59. package/docs/api/simplificationEngine.md +98 -0
  60. package/docs/api/simplificationRules.md +77 -0
  61. package/docs/api/simplificationUtils.md +64 -0
  62. package/docs/api/transcribe.md +43 -0
  63. package/docs/api-reference.md +85 -0
  64. package/docs/index.html +454 -0
  65. package/docs/user-guide.md +9 -0
  66. package/index.js +67 -0
  67. package/omd/config/omdConfigManager.js +267 -0
  68. package/omd/core/index.js +150 -0
  69. package/omd/core/omdEquationStack.js +347 -0
  70. package/omd/core/omdUtilities.js +115 -0
  71. package/omd/display/omdDisplay.js +443 -0
  72. package/omd/display/omdToolbar.js +502 -0
  73. package/omd/nodes/omdBinaryExpressionNode.js +460 -0
  74. package/omd/nodes/omdConstantNode.js +142 -0
  75. package/omd/nodes/omdEquationNode.js +1223 -0
  76. package/omd/nodes/omdEquationSequenceNode.js +1273 -0
  77. package/omd/nodes/omdFunctionNode.js +352 -0
  78. package/omd/nodes/omdGroupNode.js +68 -0
  79. package/omd/nodes/omdLeafNode.js +77 -0
  80. package/omd/nodes/omdNode.js +557 -0
  81. package/omd/nodes/omdOperationDisplayNode.js +322 -0
  82. package/omd/nodes/omdOperatorNode.js +109 -0
  83. package/omd/nodes/omdParenthesisNode.js +293 -0
  84. package/omd/nodes/omdPowerNode.js +236 -0
  85. package/omd/nodes/omdRationalNode.js +295 -0
  86. package/omd/nodes/omdSqrtNode.js +308 -0
  87. package/omd/nodes/omdUnaryExpressionNode.js +178 -0
  88. package/omd/nodes/omdVariableNode.js +123 -0
  89. package/omd/simplification/omdSimplification.js +171 -0
  90. package/omd/simplification/omdSimplificationEngine.js +886 -0
  91. package/omd/simplification/package.json +6 -0
  92. package/omd/simplification/rules/binaryRules.js +1037 -0
  93. package/omd/simplification/rules/functionRules.js +111 -0
  94. package/omd/simplification/rules/index.js +48 -0
  95. package/omd/simplification/rules/parenthesisRules.js +19 -0
  96. package/omd/simplification/rules/powerRules.js +143 -0
  97. package/omd/simplification/rules/rationalRules.js +475 -0
  98. package/omd/simplification/rules/sqrtRules.js +48 -0
  99. package/omd/simplification/rules/unaryRules.js +37 -0
  100. package/omd/simplification/simplificationRules.js +32 -0
  101. package/omd/simplification/simplificationUtils.js +1056 -0
  102. package/omd/step-visualizer/omdStepVisualizer.js +597 -0
  103. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
  104. package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
  105. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
  106. package/omd/utils/omdNodeOverlay.js +638 -0
  107. package/omd/utils/omdPopup.js +1084 -0
  108. package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
  109. package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
  110. package/omd/utils/omdTranscriptionService.js +125 -0
  111. package/omd/utils/omdTreeDiff.js +734 -0
  112. package/package.json +46 -0
  113. package/src/index.js +62 -0
  114. package/src/json-schemas.md +109 -0
  115. package/src/omd-json-samples.js +115 -0
  116. package/src/omd.js +109 -0
  117. package/src/omdApp.js +391 -0
  118. package/src/omdAppCanvas.js +336 -0
  119. package/src/omdBalanceHanger.js +172 -0
  120. package/src/omdColor.js +13 -0
  121. package/src/omdCoordinatePlane.js +467 -0
  122. package/src/omdEquation.js +125 -0
  123. package/src/omdExpression.js +104 -0
  124. package/src/omdFunction.js +113 -0
  125. package/src/omdMetaExpression.js +287 -0
  126. package/src/omdNaturalExpression.js +564 -0
  127. package/src/omdNode.js +384 -0
  128. package/src/omdNumber.js +53 -0
  129. package/src/omdNumberLine.js +107 -0
  130. package/src/omdNumberTile.js +119 -0
  131. package/src/omdOperator.js +73 -0
  132. package/src/omdPowerExpression.js +92 -0
  133. package/src/omdProblem.js +55 -0
  134. package/src/omdRatioChart.js +232 -0
  135. package/src/omdRationalExpression.js +115 -0
  136. package/src/omdSampleData.js +215 -0
  137. package/src/omdShapes.js +476 -0
  138. package/src/omdSpinner.js +148 -0
  139. package/src/omdString.js +39 -0
  140. package/src/omdTable.js +369 -0
  141. package/src/omdTapeDiagram.js +245 -0
  142. package/src/omdTerm.js +92 -0
  143. package/src/omdTileEquation.js +349 -0
  144. package/src/omdVariable.js +51 -0
@@ -0,0 +1,1084 @@
1
+ import { jsvgGroup, jsvgRect, jsvgButton, jsvgTextBox } from '@teachinglab/jsvg';
2
+ /**
3
+ * omdPopup - Handles popup creation and management for node overlays
4
+ */
5
+ export class omdPopup {
6
+ constructor(targetNode, parentElement, options = {}) {
7
+ this.targetNode = targetNode;
8
+ this.parentElement = parentElement;
9
+ this.options = {
10
+ editable: true,
11
+ animationDuration: 200,
12
+ ...options
13
+ };
14
+
15
+ // State
16
+ this.popup = null;
17
+ this.popupBackground = null;
18
+ this.popupTextInput = null;
19
+ this.penCanvas = null;
20
+ this.penCanvasCleanup = null;
21
+ this.currentMode = 'text'; // 'text' or 'pen'
22
+ this.popupAnimationId = null;
23
+
24
+ // Buttons
25
+ this.penButton = null;
26
+ this.textButton = null;
27
+ this.clearButton = null;
28
+ this.submitButton = null;
29
+
30
+ // Callbacks
31
+ this.onValidateCallback = null;
32
+ this.onClearCallback = null;
33
+
34
+ // Layout configuration - centralized variables
35
+ this.popupWidth = 400;
36
+ this.buttonSize = 18;
37
+ this.margin = 10;
38
+ this.buttonSpacing = 4;
39
+ this.canvasMinWidth = 100;
40
+ this.canvasMinHeight = 60;
41
+ this.popupHeightMultiplier = 2;
42
+ this.targetNodeDefaultHeight = 40;
43
+ this.canvasTopOffset = this.margin;
44
+ this.canvasLeftOffset = this.margin;
45
+ }
46
+
47
+ /**
48
+ * Creates and shows the popup
49
+ * @param {number} x - X position
50
+ * @param {number} y - Y position
51
+ * @returns {Promise} Promise that resolves when animation completes
52
+ */
53
+ show(x, y) {
54
+ if (this.popup) {
55
+ return this.hide().then(() => this.show(x, y));
56
+ }
57
+
58
+ this._createPopup();
59
+ this._positionPopup(x, y);
60
+ this.parentElement.addChild(this.popup);
61
+
62
+ // Start invisible and animate in
63
+ this.popup.setOpacity(0);
64
+ return this._animateOpacity(0, 1, this.options.animationDuration);
65
+ }
66
+
67
+ /**
68
+ * Hides the popup
69
+ * @returns {Promise} Promise that resolves when animation completes
70
+ */
71
+ hide() {
72
+ if (!this.popup) return Promise.resolve();
73
+
74
+ // Store original opacities for animation
75
+ const originalPopupOpacity = this.popup.opacity;
76
+ const originalCanvasOpacity = this.penCanvas?.container?.style.opacity || '1';
77
+ const originalTextInputOpacity = this.popupTextInput?.div?.style.opacity || '1';
78
+
79
+ // Ensure canvas and text input are visible for animation
80
+ if (this.penCanvas && this.penCanvas.container) {
81
+ this.penCanvas.container.style.display = 'block';
82
+ this.penCanvas.container.style.opacity = originalCanvasOpacity;
83
+ }
84
+ if (this.popupTextInput && this.popupTextInput.div) {
85
+ this.popupTextInput.div.style.display = 'flex';
86
+ this.popupTextInput.div.style.opacity = originalTextInputOpacity;
87
+ }
88
+
89
+ // Animate all elements together
90
+ const duration = this.options.animationDuration || 300;
91
+ const startTime = performance.now();
92
+
93
+ const animate = (currentTime) => {
94
+ const elapsed = currentTime - startTime;
95
+ const progress = Math.min(elapsed / duration, 1);
96
+
97
+ // Easing function for smooth animation
98
+ const easeOut = 1 - Math.pow(1 - progress, 3);
99
+ const currentOpacity = originalPopupOpacity * (1 - easeOut);
100
+
101
+ // Animate popup
102
+ if (this.popup) {
103
+ this.popup.setOpacity(currentOpacity);
104
+ }
105
+
106
+ // Animate canvas with proper opacity setting
107
+ if (this.penCanvas && this.penCanvas.container) {
108
+ this.penCanvas.container.style.opacity = currentOpacity;
109
+ }
110
+
111
+ // Animate text input with proper opacity setting
112
+ if (this.popupTextInput && this.popupTextInput.div) {
113
+ this.popupTextInput.div.style.opacity = currentOpacity;
114
+ }
115
+
116
+ if (progress < 1) {
117
+ this.popupAnimationId = requestAnimationFrame(animate);
118
+ } else {
119
+ // Animation complete - hide and cleanup
120
+ if (this.penCanvas && this.penCanvas.container) {
121
+ this.penCanvas.container.style.display = 'none';
122
+ }
123
+ if (this.popupTextInput && this.popupTextInput.div) {
124
+ this.popupTextInput.div.style.display = 'none';
125
+ }
126
+ this._cleanup();
127
+ }
128
+ };
129
+
130
+ this.popupAnimationId = requestAnimationFrame(animate);
131
+
132
+ return new Promise((resolve) => {
133
+ setTimeout(resolve, duration);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Toggles popup visibility
139
+ * @param {number} x - X position for showing
140
+ * @param {number} y - Y position for showing
141
+ * @returns {Promise} Promise that resolves when animation completes
142
+ */
143
+ toggle(x, y) {
144
+ if (this.popup && this.popup.visible) {
145
+ return this.hide();
146
+ } else {
147
+ return this.show(x, y);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Sets the validation callback
153
+ * @param {Function} callback - Function to call for validation
154
+ */
155
+ setValidationCallback(callback) {
156
+ this.onValidateCallback = callback;
157
+ }
158
+
159
+ /**
160
+ * Sets the clear callback
161
+ * @param {Function} callback - Function to call when clearing
162
+ */
163
+ setClearCallback(callback) {
164
+ this.onClearCallback = callback;
165
+ }
166
+
167
+ /**
168
+ * Gets the current input value
169
+ * @returns {string} Current input value
170
+ */
171
+ getValue() {
172
+ // If we have transcribed text from pen mode, return it
173
+ if (this.transcribedText) {
174
+ const text = this.transcribedText;
175
+ // Clear the transcribed text after returning it
176
+ this.transcribedText = null;
177
+ return text;
178
+ }
179
+
180
+ if (this.currentMode === 'text' && this.popupTextInput) {
181
+ return this.popupTextInput.getText();
182
+ }
183
+ return '';
184
+ }
185
+
186
+ /**
187
+ * Sets the input value
188
+ * @param {string} value - Value to set
189
+ */
190
+ setValue(value) {
191
+ if (this.currentMode === 'text' && this.popupTextInput) {
192
+ this.popupTextInput.setText(value);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Switches between text and pen modes
198
+ * @param {string} mode - 'text' or 'pen'
199
+ */
200
+ switchToMode(mode) {
201
+ if (this.currentMode === mode || !this.popup) {
202
+ return;
203
+ }
204
+ this.currentMode = mode;
205
+ if (mode === 'pen') {
206
+ this._showPenMode();
207
+ } else {
208
+ this._showTextMode();
209
+ }
210
+ // Update button states
211
+ this._updateButtonStates();
212
+ }
213
+
214
+ /**
215
+ * Creates the popup structure
216
+ * @private
217
+ */
218
+ _createPopup() {
219
+ const popupHeight = (this.targetNode?.height || this.targetNodeDefaultHeight) * this.popupHeightMultiplier;
220
+
221
+ // Create popup container
222
+ this.popup = new jsvgLayoutGroup();
223
+
224
+ // Create popup background
225
+ this.popupBackground = new jsvgRect();
226
+ this.popupBackground.setWidthAndHeight(this.popupWidth, popupHeight);
227
+ this.popupBackground.setFillColor('white');
228
+ this.popupBackground.setStrokeColor('black');
229
+ this.popupBackground.setStrokeWidth(2);
230
+ this.popupBackground.setCornerRadius(8);
231
+ this.popup.addChild(this.popupBackground);
232
+
233
+ // Create buttons
234
+ this._createButtons(this.popupWidth, popupHeight, this.buttonSize, this.margin, this.buttonSpacing);
235
+
236
+ // Create text input (default mode)
237
+ this._createTextInput(this.popupWidth, popupHeight, this.margin);
238
+
239
+ // Set initial mode
240
+ this.currentMode = 'text';
241
+ this._updateButtonStates();
242
+ }
243
+
244
+ /**
245
+ * Creates the popup buttons
246
+ * @private
247
+ */
248
+ _createButtons(popupWidth, popupHeight, buttonSize, margin, buttonSpacing) {
249
+ const buttonX = popupWidth - buttonSize - margin;
250
+
251
+ // Clear button
252
+ const clearButtonY = popupHeight - (buttonSize * 2) - margin - buttonSpacing;
253
+ this.clearButton = new jsvgButton();
254
+ this.clearButton.setText("C");
255
+ this.clearButton.setWidthAndHeight(buttonSize, buttonSize);
256
+ this.clearButton.setFillColor('#E65423');
257
+ this.clearButton.setFontColor('white');
258
+ this.clearButton.buttonText.setFontWeight('bold');
259
+ this.clearButton.setPosition(buttonX, clearButtonY);
260
+ this.clearButton.setClickCallback(() => {
261
+ if (this.currentMode === 'pen' && this.penCanvas) {
262
+ // Clear the canvas
263
+ this.penCanvas.clear();
264
+ } else if (this.currentMode === 'text' && this.popupTextInput) {
265
+ // Clear the text input
266
+ this.popupTextInput.setText('');
267
+ }
268
+ // Also call the external clear callback if provided
269
+ if (this.onClearCallback) {
270
+ this.onClearCallback();
271
+ }
272
+ });
273
+ this.popup.addChild(this.clearButton);
274
+
275
+ // Submit button
276
+ const submitButtonY = popupHeight - buttonSize - margin;
277
+ this.submitButton = new jsvgButton();
278
+ this.submitButton.setText("✓");
279
+ this.submitButton.setWidthAndHeight(buttonSize, buttonSize);
280
+ this.submitButton.setFillColor('#2ECC71');
281
+ this.submitButton.setFontColor('white');
282
+ this.submitButton.buttonText.setFontWeight('bold');
283
+ this.submitButton.setPosition(buttonX, submitButtonY);
284
+ this.submitButton.setClickCallback(() => {
285
+ if (this.currentMode === 'pen' && this.penCanvas) {
286
+ // For pen mode, transcribe the canvas first
287
+ this._setSubmitButtonLoading(true);
288
+ this._downloadCanvasAsBitmap();
289
+ } else if (this.onValidateCallback) {
290
+ // For text mode, validate directly
291
+ this.onValidateCallback();
292
+ }
293
+ });
294
+ this.popup.addChild(this.submitButton);
295
+
296
+ // Mode buttons (P and T) - positioned in bottom left, aligned with other buttons
297
+ const leftMargin = margin;
298
+
299
+ // Text button (above pen button, aligned with clear button)
300
+ this.textButton = new jsvgButton();
301
+ this.textButton.setText("T");
302
+ this.textButton.setWidthAndHeight(buttonSize, buttonSize);
303
+ this.textButton.setFillColor('#28a745');
304
+ this.textButton.setFontColor('white');
305
+ this.textButton.buttonText.setFontWeight('bold');
306
+ this.textButton.setPosition(leftMargin, clearButtonY);
307
+ this.textButton.setClickCallback(() => {
308
+ this.switchToMode('text');
309
+ });
310
+ this.popup.addChild(this.textButton);
311
+
312
+ // Pen button (bottom, aligned with submit button)
313
+ this.penButton = new jsvgButton();
314
+ this.penButton.setText("");
315
+ this.penButton.setWidthAndHeight(buttonSize, buttonSize);
316
+ this.penButton.setFillColor('#007bff');
317
+ this.penButton.setPosition(leftMargin, submitButtonY);
318
+ this.penButton.setClickCallback(() => this.switchToMode('pen'));
319
+
320
+ // Add pencil icon (same as canvas toolbar)
321
+ const pencilIconSvg = `<svg width="12" height="12" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
322
+ <path d="M13.3658 4.68008C13.7041 4.34179 13.8943 3.88294 13.8943 3.40447C13.8944 2.926 13.7044 2.4671 13.3661 2.12872C13.0278 1.79035 12.5689 1.60022 12.0905 1.60016C11.612 1.6001 11.1531 1.79011 10.8147 2.1284L2.27329 10.6718C2.12469 10.8199 2.0148 11.0023 1.95329 11.203L1.10785 13.9882C1.09131 14.0436 1.09006 14.1024 1.10423 14.1584C1.11841 14.2144 1.14748 14.2655 1.18836 14.3063C1.22924 14.3471 1.28041 14.3761 1.33643 14.3902C1.39246 14.4043 1.45125 14.403 1.50657 14.3863L4.29249 13.5415C4.49292 13.4806 4.67532 13.3713 4.82369 13.2234L13.3658 4.68008Z" stroke="white" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
323
+ <path d="M9.41443 3.52039L11.9744 6.08039" stroke="white" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
324
+ </svg>`;
325
+ const dataURI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(pencilIconSvg);
326
+ this.penButton.addImage(dataURI, 12, 12);
327
+
328
+ this.popup.addChild(this.penButton);
329
+ }
330
+
331
+ /**
332
+ * Creates the text input
333
+ * @private
334
+ */
335
+ _createTextInput(popupWidth, popupHeight, margin) {
336
+ const buttonAreaWidth = (this.buttonSize * 2) + (margin * 2) + this.margin;
337
+
338
+ // Text input covers the middle area between the left and right button areas
339
+ const inputWidth = popupWidth - buttonAreaWidth - (margin * 2);
340
+ const inputHeight = popupHeight - (margin * 2);
341
+ const inputX = this.buttonSize + (margin * 2);
342
+ const inputY = margin;
343
+
344
+ this.popupTextInput = new jsvgTextInput();
345
+ this.popupTextInput.setWidthAndHeight(inputWidth, inputHeight);
346
+ this.popupTextInput.setPosition(inputX, inputY);
347
+
348
+ // Make background transparent/invisible
349
+ this.popupTextInput.setFillColor('transparent');
350
+ this.popupTextInput.setStrokeColor('transparent');
351
+ this.popupTextInput.setStrokeWidth(0);
352
+ this.popupTextInput.setPlaceholderText("");
353
+
354
+ // Style the actual input for large centered text
355
+ if (this.popupTextInput.div) {
356
+ this.popupTextInput.div.style.border = 'none';
357
+ this.popupTextInput.div.style.outline = 'none';
358
+ this.popupTextInput.div.style.background = 'transparent';
359
+ this.popupTextInput.div.style.textAlign = 'center';
360
+ this.popupTextInput.div.style.fontSize = '48px';
361
+ this.popupTextInput.div.style.fontFamily = 'Albert Sans, Arial, sans-serif';
362
+ this.popupTextInput.div.style.resize = 'none';
363
+ this.popupTextInput.div.style.width = '100%';
364
+ this.popupTextInput.div.style.height = '100%';
365
+ this.popupTextInput.div.style.display = 'flex';
366
+ this.popupTextInput.div.style.alignItems = 'center';
367
+ this.popupTextInput.div.style.justifyContent = 'center';
368
+ this.popupTextInput.div.style.boxSizing = 'border-box';
369
+ }
370
+
371
+ // Add to popup in text mode
372
+ this.popup.addChild(this.popupTextInput);
373
+ }
374
+
375
+ /**
376
+ * Creates the pen canvas
377
+ * @private
378
+ */
379
+ _createPenCanvas() {
380
+ // Use class variables directly
381
+ const popupWidth = this.popupWidth;
382
+ const popupHeight = (this.targetNode?.height || this.targetNodeDefaultHeight) * this.popupHeightMultiplier;
383
+
384
+ // Calculate canvas dimensions based on popup size
385
+ const leftMargin = this.buttonSize;
386
+ const rightMargin = 2 * this.buttonSize + this.buttonSpacing; // Space for buttons
387
+
388
+ // Calculate canvas dimensions based on popup size
389
+ const canvasWidth = Math.max(popupWidth - leftMargin - rightMargin, this.canvasMinWidth);
390
+
391
+ // Calculate canvas height to fit in available space
392
+ const buttonAreaHeight = (this.buttonSize * 2) + this.buttonSpacing + (this.margin * 2);
393
+ const availableHeight = popupHeight - buttonAreaHeight;
394
+ const canvasHeight = Math.max(availableHeight, this.canvasMinHeight);
395
+ const canvasX = leftMargin;
396
+
397
+ // Try to create a regular HTML container first (fallback approach)
398
+ let canvasContainer;
399
+ let foreignObject = null;
400
+
401
+ // Check if we're in an SVG context
402
+ if (this.parentElement && this.parentElement.element &&
403
+ this.parentElement.element.namespaceURI === 'http://www.w3.org/2000/svg') {
404
+
405
+ // Create an SVG foreignObject to embed HTML canvas
406
+ foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
407
+ foreignObject.setAttribute('width', canvasWidth);
408
+ foreignObject.setAttribute('height', canvasHeight);
409
+ foreignObject.setAttribute('x', canvasX);
410
+ foreignObject.setAttribute('y', this.canvasTopOffset);
411
+
412
+ // Create the HTML container inside foreignObject
413
+ canvasContainer = document.createElement('div');
414
+ canvasContainer.style.width = `${canvasWidth}px`;
415
+ canvasContainer.style.height = `${canvasHeight}px`;
416
+ canvasContainer.style.border = '1px solid #ddd';
417
+ canvasContainer.style.borderRadius = '4px';
418
+ canvasContainer.style.backgroundColor = '#f8f9fa';
419
+ canvasContainer.style.margin = '0';
420
+ canvasContainer.style.padding = '0';
421
+
422
+ // Add the container to foreignObject
423
+ foreignObject.appendChild(canvasContainer);
424
+ } else {
425
+ // Create a regular HTML container
426
+ canvasContainer = document.createElement('div');
427
+ canvasContainer.style.width = `${canvasWidth}px`;
428
+ canvasContainer.style.height = `${canvasHeight}px`;
429
+ canvasContainer.style.position = 'absolute';
430
+ canvasContainer.style.left = `${canvasX}px`;
431
+ canvasContainer.style.top = `${this.canvasTopOffset}px`;
432
+ canvasContainer.style.border = 'none'; // No border
433
+ canvasContainer.style.borderRadius = '0px';
434
+ canvasContainer.style.backgroundColor = '#ffffff'; // White background
435
+ canvasContainer.style.zIndex = '1000';
436
+ canvasContainer.style.pointerEvents = 'auto';
437
+ canvasContainer.style.cursor = 'crosshair';
438
+ }
439
+
440
+ // Import our new modular canvas system
441
+ import('../../canvas/index.js').then(({ createCanvas }) => {
442
+ // Ensure we have valid positive dimensions
443
+ const finalWidth = canvasWidth;
444
+ const finalHeight = canvasHeight;
445
+
446
+ // Create the canvas using our new system
447
+ this.penCanvas = createCanvas(canvasContainer, {
448
+ width: finalWidth - this.buttonSize,
449
+ height: finalHeight,
450
+ showToolbar: false,
451
+ showGrid: false,
452
+ backgroundColor: '#ffffff', // White background
453
+ strokeWidth: 3,
454
+ strokeColor: '#000000'
455
+ });
456
+
457
+ // Store the container and foreignObject references for cleanup
458
+ this.penCanvas.container = canvasContainer;
459
+ this.penCanvas.foreignObject = foreignObject;
460
+
461
+ // Set pencil as the default tool
462
+ this.penCanvas.toolManager.setActiveTool('pencil');
463
+
464
+ // Cursor will be shown/hidden by EventManager based on mouse enter/leave
465
+
466
+ // Add event listeners for debugging
467
+ this.penCanvas.on('strokeStarted', (event) => {});
468
+ this.penCanvas.on('strokeCompleted', (event) => {});
469
+
470
+ // Add pointer event debugging
471
+ canvasContainer.addEventListener('pointerdown', (e) => {});
472
+
473
+
474
+
475
+ // Add click debugging
476
+ canvasContainer.addEventListener('click', (e) => {});
477
+
478
+ // Store cleanup function
479
+ this.penCanvasCleanup = () => {
480
+ if (this.penCanvas) {
481
+ this.penCanvas.destroy();
482
+ }
483
+ if (foreignObject && foreignObject.parentNode) {
484
+ foreignObject.parentNode.removeChild(foreignObject);
485
+ } else if (canvasContainer && canvasContainer.parentNode) {
486
+ canvasContainer.parentNode.removeChild(canvasContainer);
487
+ }
488
+ };
489
+
490
+ // If we're currently in pen mode, show the canvas
491
+ if (this.currentMode === 'pen' && this.popup) {
492
+ this._addCanvasToParent(foreignObject || canvasContainer);
493
+ }
494
+
495
+ }).catch(console.error);
496
+ }
497
+
498
+ /**
499
+ * Shows pen mode
500
+ * @private
501
+ */
502
+ _showPenMode() {
503
+ // Hide text input by making it invisible instead of removing it
504
+ if (this.popupTextInput && this.popupTextInput.div) {
505
+ this.popupTextInput.div.style.display = 'none';
506
+ this.popupTextInput.div.style.visibility = 'hidden';
507
+ }
508
+
509
+ // Create pen canvas if it doesn't exist
510
+ if (!this.penCanvas) {
511
+ this._createPenCanvas();
512
+ } else {
513
+ this._addCanvasToParent();
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Shows text mode
519
+ * @private
520
+ */
521
+ _showTextMode() {
522
+ // Hide pen canvas by removing its element
523
+ if (this.penCanvas) {
524
+ const element = this.penCanvas.foreignObject || this.penCanvas.container;
525
+ if (element && element.parentNode) {
526
+ element.parentNode.removeChild(element);
527
+ }
528
+ }
529
+
530
+ // Show text input and ensure it's visible and focused
531
+ if (this.popupTextInput && this.popup) {
532
+ // Make sure text input is visible
533
+ if (this.popupTextInput.div) {
534
+ this.popupTextInput.div.style.display = 'flex';
535
+ this.popupTextInput.div.style.visibility = 'visible';
536
+ this.popupTextInput.div.style.opacity = '1';
537
+ }
538
+
539
+ // Add to popup (jsvgLayoutGroup handles duplicates automatically)
540
+ this.popup.addChild(this.popupTextInput);
541
+
542
+ // Focus the text input after a short delay to ensure it's rendered
543
+ setTimeout(() => {
544
+ if (this.popupTextInput.div) {
545
+ this.popupTextInput.div.focus();
546
+ }
547
+ }, 100);
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Adds canvas to parent element (same level as popup)
553
+ * @private
554
+ */
555
+ _addCanvasToParent(element = null) {
556
+ // If no element provided and canvas already exists, re-add it to parent
557
+ if (!element && this.penCanvas && this.penCanvas.container) {
558
+ // Re-add the canvas container to the parent
559
+ if (this.penCanvas.container.parentNode) {
560
+ this.penCanvas.container.parentNode.removeChild(this.penCanvas.container);
561
+ }
562
+ document.body.appendChild(this.penCanvas.container);
563
+ this._updateCanvasPosition();
564
+ return;
565
+ }
566
+
567
+ if (this.penCanvas && this.popup && this.parentElement) {
568
+ const popupX = this.popup.xpos || 0;
569
+ const popupY = this.popup.ypos || 0;
570
+
571
+ // If we have an element to add
572
+ if (element) {
573
+ // Check if it's a foreignObject (SVG) or regular HTML element
574
+ if (element.namespaceURI === 'http://www.w3.org/2000/svg') {
575
+ // SVG foreignObject
576
+ element.setAttribute('x', popupX + this.canvasLeftOffset);
577
+ element.setAttribute('y', popupY + this.canvasTopOffset);
578
+
579
+ // Add the foreignObject to the parent element
580
+ if (this.parentElement.element) {
581
+ this.parentElement.element.appendChild(element);
582
+ } else if (this.parentElement.appendChild) {
583
+ this.parentElement.appendChild(element);
584
+ }
585
+ } else {
586
+ // Regular HTML element
587
+ const popupRect = this.popup.svgObject ? this.popup.svgObject.getBoundingClientRect() : null;
588
+
589
+ if (popupRect) {
590
+ // Calculate position to match popup exactly
591
+ const leftButtonArea = this.buttonSize + (this.margin * 2);
592
+ const rightButtonArea = (this.buttonSize * 2) + (this.margin * 2) + this.margin;
593
+
594
+ // Position canvas in the middle content area
595
+ const absoluteX = popupRect.left + leftButtonArea;
596
+ const absoluteY = popupRect.top + this.margin;
597
+
598
+ element.style.left = `${absoluteX}px`;
599
+ element.style.top = `${absoluteY}px`;
600
+ element.style.position = 'fixed';
601
+ element.style.zIndex = '9999';
602
+ element.style.pointerEvents = 'auto';
603
+
604
+ // Set canvas size to match popup content area
605
+ const contentWidth = popupRect.width - leftButtonArea - rightButtonArea;
606
+ element.style.width = `${contentWidth}px`;
607
+
608
+ // Store popup reference for resize handling
609
+ this.penCanvas.popupRect = popupRect;
610
+
611
+ // Add resize observer to track popup changes
612
+ this._setupResizeObserver();
613
+
614
+ } else {
615
+ // Fallback positioning
616
+ const absoluteX = (window.innerWidth - 280) / 2;
617
+ const absoluteY = (window.innerHeight - 60) / 2;
618
+
619
+ element.style.left = `${absoluteX}px`;
620
+ element.style.top = `${absoluteY}px`;
621
+ element.style.position = 'fixed';
622
+ element.style.zIndex = '9999';
623
+ element.style.pointerEvents = 'auto';
624
+ }
625
+
626
+ // Add to document body for HTML approach
627
+ document.body.appendChild(element);
628
+ }
629
+ }
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Updates button visual states based on current mode
635
+ * @private
636
+ */
637
+ _updateButtonStates() {
638
+ if (!this.penButton || !this.textButton) return;
639
+
640
+ if (this.currentMode === 'pen') {
641
+ this.penButton.setFillColor('#2980B9'); // Darker blue for active
642
+ this.textButton.setFillColor('#9B59B6'); // Normal purple
643
+ } else {
644
+ this.penButton.setFillColor('#3498DB'); // Normal blue
645
+ this.textButton.setFillColor('#8E44AD'); // Darker purple for active
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Positions the popup
651
+ * @private
652
+ */
653
+ _positionPopup(x, y) {
654
+ if (!this.popup) return;
655
+ this.popup.setPosition(x, y);
656
+ }
657
+
658
+ /**
659
+ * Animates popup opacity
660
+ * @private
661
+ */
662
+ _animateOpacity(fromOpacity, toOpacity, duration) {
663
+ return new Promise((resolve) => {
664
+ if (!this.popup) {
665
+ resolve();
666
+ return;
667
+ }
668
+
669
+ if (this.popupAnimationId) {
670
+ cancelAnimationFrame(this.popupAnimationId);
671
+ }
672
+
673
+ const startTime = performance.now();
674
+ const deltaOpacity = toOpacity - fromOpacity;
675
+
676
+ const animate = (currentTime) => {
677
+ const elapsed = currentTime - startTime;
678
+ const progress = Math.min(elapsed / duration, 1);
679
+ const easedProgress = 1 - Math.pow(1 - progress, 3);
680
+ const currentOpacity = fromOpacity + (deltaOpacity * easedProgress);
681
+
682
+ this.popup.setOpacity(currentOpacity);
683
+
684
+ if (progress < 1) {
685
+ this.popupAnimationId = requestAnimationFrame(animate);
686
+ } else {
687
+ this.popupAnimationId = null;
688
+ resolve();
689
+ }
690
+ };
691
+
692
+ this.popupAnimationId = requestAnimationFrame(animate);
693
+ });
694
+ }
695
+
696
+ /**
697
+ * Flashes the popup background to indicate validation result
698
+ * @param {boolean} isValid - Whether validation was successful
699
+ */
700
+ flashValidation(isValid) {
701
+ if (!this.popupBackground) return;
702
+
703
+ const flashColor = isValid ? '#E8F5E8' : '#FFE6E6';
704
+ this._flashAllElements(flashColor);
705
+ }
706
+
707
+ /**
708
+ * Flash all elements with the same color and return to white
709
+ * @private
710
+ */
711
+ _flashAllElements(flashColor) {
712
+ // Flash popup background
713
+ this.popupBackground.setFillColor(flashColor);
714
+
715
+ // Flash canvas container
716
+ if (this.penCanvas && this.penCanvas.container) {
717
+ this.penCanvas.container.style.backgroundColor = flashColor;
718
+ }
719
+
720
+ // Flash canvas drawing area
721
+ if (this.penCanvas && this.penCanvas.svg) {
722
+ this.penCanvas.svg.style.backgroundColor = flashColor;
723
+ }
724
+
725
+ // Return all elements to white after 300ms
726
+ setTimeout(() => {
727
+ this.popupBackground.setFillColor('white');
728
+
729
+ if (this.penCanvas && this.penCanvas.container) {
730
+ this.penCanvas.container.style.backgroundColor = 'white';
731
+ }
732
+
733
+ if (this.penCanvas && this.penCanvas.svg) {
734
+ this.penCanvas.svg.style.backgroundColor = 'white';
735
+ }
736
+ }, 300);
737
+ }
738
+
739
+ /**
740
+ * Checks if two mathematical expressions are equivalent
741
+ * @param {string} expr1 - First expression
742
+ * @param {string} expr2 - Second expression
743
+ * @returns {boolean} True if expressions are mathematically equivalent
744
+ */
745
+ areExpressionsEquivalent(expr1, expr2) {
746
+ // Robust equivalence: compare evaluated results for random variable assignments
747
+ if (!window.math || !window.math.simplify || !window.math.parse) {
748
+ return false;
749
+ }
750
+ try {
751
+ const expr1Trimmed = expr1.trim();
752
+ const expr2Trimmed = expr2.trim();
753
+ const node1 = window.math.simplify(expr1Trimmed);
754
+ const node2 = window.math.simplify(expr2Trimmed);
755
+ // For debugging, log the stringified forms
756
+ console.log('Simplified expressions:', { simplified1: node1.toString(), simplified2: node2.toString() });
757
+ // If ASTs match, return true
758
+ if (node1.equals(node2)) return true;
759
+
760
+ // Otherwise, compare evaluated results for random variable assignments
761
+ // Find all variable names
762
+ const getVars = expr => {
763
+ const node = window.math.parse(expr);
764
+ const vars = new Set();
765
+ node.traverse(n => {
766
+ if (n.isSymbolNode) vars.add(n.name);
767
+ });
768
+ return Array.from(vars);
769
+ };
770
+ const vars = Array.from(new Set([...getVars(expr1Trimmed), ...getVars(expr2Trimmed)]));
771
+ if (vars.length === 0) {
772
+ // No variables, just compare evaluated results
773
+ return node1.evaluate() === node2.evaluate();
774
+ }
775
+ // Try several random assignments
776
+ for (let i = 0; i < 100; i++) {
777
+ const scope = {};
778
+ for (const v of vars) {
779
+ scope[v] = Math.floor(Math.random() * 1000 + 1); // random int 1-10
780
+ }
781
+ const val1 = node1.evaluate(scope);
782
+ const val2 = node2.evaluate(scope);
783
+ if (Math.abs(val1 - val2) > 1e-9) return false;
784
+ }
785
+ return true;
786
+ } catch (e) {
787
+ return false;
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Cleanup popup and all associated elements
793
+ * @private
794
+ */
795
+ _cleanup() {
796
+ if (this.popup && this.parentElement) {
797
+ this.parentElement.removeChild(this.popup);
798
+ }
799
+
800
+ // Remove pen canvas from parent if it exists
801
+ if (this.penCanvas) {
802
+ // Remove both foreignObject and container if they exist
803
+ if (this.penCanvas.foreignObject && this.penCanvas.foreignObject.parentNode) {
804
+ this.penCanvas.foreignObject.parentNode.removeChild(this.penCanvas.foreignObject);
805
+ }
806
+
807
+ if (this.penCanvas.container && this.penCanvas.container.parentNode) {
808
+ this.penCanvas.container.parentNode.removeChild(this.penCanvas.container);
809
+ }
810
+ }
811
+
812
+ // Clean up pen canvas
813
+ if (this.penCanvasCleanup) {
814
+ this.penCanvasCleanup();
815
+ this.penCanvasCleanup = null;
816
+ }
817
+
818
+ // Clean up resize observer
819
+ if (this.resizeObserver) {
820
+ this.resizeObserver.disconnect();
821
+ this.resizeObserver = null;
822
+ }
823
+
824
+ // Clean up animation
825
+ if (this.popupAnimationId) {
826
+ cancelAnimationFrame(this.popupAnimationId);
827
+ this.popupAnimationId = null;
828
+ }
829
+
830
+ // Reset all references
831
+ this.popup = null;
832
+ this.popupBackground = null;
833
+ this.popupTextInput = null;
834
+ this.penCanvas = null;
835
+ this.penButton = null;
836
+ this.textButton = null;
837
+ this.clearButton = null;
838
+ this.submitButton = null;
839
+ }
840
+
841
+ /**
842
+ * Destroys the popup completely
843
+ */
844
+ destroy() {
845
+ return this.hide();
846
+ }
847
+
848
+ /**
849
+ * Debug function to test canvas positioning
850
+ */
851
+ debugCanvasPosition() {
852
+ if (this.penCanvas && this.penCanvas.container) {
853
+ const container = this.penCanvas.container;
854
+ // Flash the border to make it visible
855
+ container.style.border = '3px solid #00ff00';
856
+ setTimeout(() => {
857
+ container.style.border = '2px solid #ff0000';
858
+ }, 1000);
859
+ }
860
+ }
861
+
862
+ /**
863
+ * Reposition canvas to center of screen
864
+ */
865
+ centerCanvas() {
866
+ if (this.penCanvas && this.penCanvas.container) {
867
+ const container = this.penCanvas.container;
868
+ const centerX = (window.innerWidth - 280) / 2;
869
+ const centerY = (window.innerHeight - 60) / 2;
870
+ container.style.left = `${centerX}px`;
871
+ container.style.top = `${centerY}px`;
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Setup resize observer to track popup changes
877
+ * @private
878
+ */
879
+ _setupResizeObserver() {
880
+ if (!this.penCanvas || !this.popup || !this.popup.svgObject) return;
881
+
882
+ // Create a resize observer to track popup changes
883
+ this.resizeObserver = new ResizeObserver((entries) => {
884
+ for (const entry of entries) {
885
+ this._updateCanvasPosition();
886
+ }
887
+ });
888
+
889
+ // Observe the popup element
890
+ this.resizeObserver.observe(this.popup.svgObject);
891
+
892
+ // Also observe the parent element for position changes
893
+ if (this.parentElement && this.parentElement.svgObject) {
894
+ this.resizeObserver.observe(this.parentElement.svgObject);
895
+ }
896
+
897
+ // Also observe the document body for zoom changes
898
+ this.resizeObserver.observe(document.body);
899
+ }
900
+
901
+ /**
902
+ * Update canvas position to match popup
903
+ * @private
904
+ */
905
+ _updateCanvasPosition() {
906
+ if (!this.penCanvas || !this.penCanvas.container || !this.popup) return;
907
+
908
+ const container = this.penCanvas.container;
909
+ const popupRect = this.popup.svgObject ? this.popup.svgObject.getBoundingClientRect() : null;
910
+
911
+ if (popupRect && popupRect.width > 0 && popupRect.height > 0) {
912
+ // Calculate button areas based on popup dimensions
913
+ const leftButtonArea = this.buttonSize + (this.margin * 2);
914
+ const rightButtonArea = (this.buttonSize * 2) + (this.margin * 2) + this.margin;
915
+
916
+ // Calculate content area within popup
917
+ const contentWidth = Math.max(popupRect.width - leftButtonArea - rightButtonArea, this.canvasMinWidth);
918
+ const contentHeight = Math.max(popupRect.height - (this.margin * 2) - (this.buttonSize * 2) - this.buttonSpacing, this.canvasMinHeight);
919
+
920
+ // Position canvas within popup bounds
921
+ const absoluteX = popupRect.left + leftButtonArea;
922
+ const absoluteY = popupRect.top + this.margin;
923
+
924
+ // Ensure canvas doesn't exceed popup bounds
925
+ const maxWidth = popupRect.width - leftButtonArea - rightButtonArea;
926
+ const maxHeight = popupRect.height - (this.margin * 2) - (this.buttonSize * 2) - this.buttonSpacing;
927
+
928
+ const finalWidth = Math.min(Math.max(contentWidth, this.canvasMinWidth), maxWidth);
929
+ const finalHeight = Math.min(Math.max(contentHeight, this.canvasMinHeight), maxHeight);
930
+
931
+ container.style.left = `${absoluteX}px`;
932
+ container.style.top = `${absoluteY}px`;
933
+ container.style.width = `${finalWidth}px`;
934
+ container.style.height = `${finalHeight}px`;
935
+ }
936
+ }
937
+
938
+ /**
939
+ * Download canvas as bitmap and transcribe
940
+ * @private
941
+ */
942
+ async _downloadCanvasAsBitmap() {
943
+ if (!this.penCanvas) {
944
+ return;
945
+ }
946
+ try {
947
+ // Get the canvas SVG element
948
+ const svgElement = this.penCanvas.svg;
949
+ if (!svgElement) {
950
+ return;
951
+ }
952
+
953
+ // Create a canvas element for conversion
954
+ const canvas = document.createElement('canvas');
955
+ const ctx = canvas.getContext('2d');
956
+
957
+ // Set canvas size to match SVG
958
+ const svgRect = svgElement.getBoundingClientRect();
959
+ canvas.width = svgRect.width;
960
+ canvas.height = svgRect.height;
961
+
962
+ // Convert SVG to data URL
963
+ const svgData = new XMLSerializer().serializeToString(svgElement);
964
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
965
+ const url = URL.createObjectURL(svgBlob);
966
+
967
+ // Create an image from the SVG
968
+ const img = new Image();
969
+ img.onload = async () => {
970
+ // Draw the image to canvas
971
+ ctx.drawImage(img, 0, 0);
972
+
973
+ // Convert to blob and transcribe (no download)
974
+ canvas.toBlob(async (blob) => {
975
+ URL.revokeObjectURL(url);
976
+ await this._transcribeCanvas(blob);
977
+ }, 'image/png');
978
+ };
979
+
980
+ img.src = url;
981
+
982
+ } catch (error) {
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Transcribe canvas content
988
+ * @private
989
+ */
990
+ async _transcribeCanvas(imageBlob) {
991
+ try {
992
+ // Import transcription service
993
+ const { omdTranscriptionService } = await import('./omdTranscriptionService.js');
994
+ // Create transcription service instance (no API keys needed - server handles them)
995
+ const transcriptionService = new omdTranscriptionService({
996
+ defaultProvider: 'gemini'
997
+ });
998
+
999
+ // Check if service is available
1000
+ if (!transcriptionService.isAvailable()) {
1001
+ return;
1002
+ }
1003
+
1004
+ // Transcribe with fallback
1005
+ const result = await transcriptionService.transcribeWithFallback(imageBlob, {
1006
+ prompt: 'Transcribe this handwritten mathematical expression. Return ONLY the pure mathematical expression with no formatting, no LaTeX, no dollar signs, no explanations. Use ^ for powers (e.g., 3^2), use / for fractions (e.g., (2x+1)/(x-3)), use * for multiplication, use + and - for addition/subtraction. Return only the expression.'
1007
+ });
1008
+ console.log('Transcription result:', result);
1009
+ if (result.text) {
1010
+ this._setSubmitButtonLoading(false);
1011
+ this.flashValidation(true);
1012
+ this.transcribedText = result.text;
1013
+ if (this.onValidateCallback) {
1014
+ this.onValidateCallback();
1015
+ }
1016
+ } else {
1017
+ this._setSubmitButtonLoading(false);
1018
+ }
1019
+ } catch (error) {
1020
+ this.flashValidation(false);
1021
+ this._setSubmitButtonLoading(false);
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * Set submit button loading state
1027
+ * @private
1028
+ */
1029
+ _setSubmitButtonLoading(isLoading) {
1030
+ if (!this.submitButton) return;
1031
+
1032
+ if (isLoading) {
1033
+ // Start blinking animation
1034
+ this._startBlinkingAnimation();
1035
+ } else {
1036
+ // Stop blinking and restore original state
1037
+ this._stopBlinkingAnimation();
1038
+ this.submitButton.setText("✓");
1039
+ this.submitButton.setFillColor('#2ECC71');
1040
+ this.submitButton.setFontColor('white');
1041
+ }
1042
+ }
1043
+
1044
+ /**
1045
+ * Start blinking animation for submit button
1046
+ * @private
1047
+ */
1048
+ _startBlinkingAnimation() {
1049
+ if (!this.submitButton) return;
1050
+
1051
+ let isOrange = true;
1052
+ const blink = () => {
1053
+ if (isOrange) {
1054
+ this.submitButton.setFillColor('#FFA500'); // Orange
1055
+ } else {
1056
+ this.submitButton.setFillColor('#2ECC71'); // Green
1057
+ }
1058
+ isOrange = !isOrange;
1059
+
1060
+ this.blinkAnimationId = setTimeout(blink, 300); // Blink every 300ms
1061
+ };
1062
+
1063
+ // Start blinking immediately
1064
+ blink();
1065
+ }
1066
+
1067
+ /**
1068
+ * Stop blinking animation for submit button
1069
+ * @private
1070
+ */
1071
+ _stopBlinkingAnimation() {
1072
+ if (this.blinkAnimationId) {
1073
+ clearTimeout(this.blinkAnimationId);
1074
+ this.blinkAnimationId = null;
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Reposition canvas relative to popup
1080
+ */
1081
+ repositionCanvasRelativeToPopup() {
1082
+ this._updateCanvasPosition();
1083
+ }
1084
+ }