@teachinglab/omd 0.2.8 → 0.2.9

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.
@@ -1,5 +1,5 @@
1
1
  import { BoundingBox } from '../utils/boundingBox.js';
2
-
2
+ import { jsvgPath } from '@teachinglab/jsvg';
3
3
  /**
4
4
  * Represents a drawing stroke made up of connected points
5
5
  */
@@ -28,6 +28,13 @@ export class omdDisplay {
28
28
  this.svg = new jsvgContainer();
29
29
  this.node = null;
30
30
 
31
+ // Internal guards to prevent recursive resize induced growth
32
+ this._suppressResizeObserver = false; // When true, _handleResize is a no-op
33
+ this._lastViewbox = null; // Cache last applied viewBox string
34
+ this._lastContentExtents = null; // Cache last measured content extents to detect real growth
35
+ this._viewboxLocked = false; // When true, suppress micro growth adjustments
36
+ this._viewboxLockThreshold = 8; // Require at least 8px growth once locked
37
+
31
38
  // Set up the SVG
32
39
  this._setupSVG();
33
40
  }
@@ -65,8 +72,13 @@ export class omdDisplay {
65
72
  }
66
73
 
67
74
  _handleResize() {
75
+ if (this._suppressResizeObserver) return; // Prevent re-entrant resize loops
68
76
  const width = this.container.offsetWidth;
69
77
  const height = this.container.offsetHeight;
78
+ // Skip if size unchanged; avoids loops where internal changes trigger observer without real container delta
79
+ if (this._lastContainerWidth === width && this._lastContainerHeight === height) return;
80
+ this._lastContainerWidth = width;
81
+ this._lastContainerHeight = height;
70
82
  this.svg.setViewbox(width, height);
71
83
 
72
84
  if (this.options.centerContent && this.node) {
@@ -132,8 +144,50 @@ export class omdDisplay {
132
144
  const desiredW = Math.max(curW, desiredRight - desiredX);
133
145
  const desiredH = Math.max(curH, desiredBottom - desiredY);
134
146
 
135
- if (desiredW !== curW || desiredH !== curH) {
136
- this.svg.svgObject.setAttribute('viewBox', `${desiredX} ${desiredY} ${desiredW} ${desiredH}`);
147
+ // Guard: If the desired size change is negligible (< 0.5px), skip.
148
+ const widthDelta = Math.abs(desiredW - curW);
149
+ const heightDelta = Math.abs(desiredH - curH);
150
+
151
+ // Safety cap to avoid runaway expansion due to logic errors.
152
+ const MAX_DIM = 10000; // arbitrary large but finite limit
153
+ if (desiredW > MAX_DIM || desiredH > MAX_DIM) {
154
+ console.warn('omdDisplay: viewBox growth capped to prevent runaway expansion', desiredW, desiredH);
155
+ return;
156
+ }
157
+
158
+ if (widthDelta < 0.5 && heightDelta < 0.5) return;
159
+
160
+ // Detect repeated growth with identical content extents (suggests feedback loop)
161
+ const curExtSignature = `${minX},${minY},${maxX},${maxY}`;
162
+ if (this._lastContentExtents === curExtSignature && heightDelta > 0 && desiredH > curH) {
163
+ return; // content unchanged, skip
164
+ }
165
+
166
+ // If locked, only allow substantial growth
167
+ if (this._viewboxLocked) {
168
+ const growW = desiredW - curW;
169
+ const growH = desiredH - curH;
170
+ if (growW < this._viewboxLockThreshold && growH < this._viewboxLockThreshold) {
171
+ return; // ignore micro growth attempts
172
+ }
173
+ }
174
+
175
+ const newViewBox = `${desiredX} ${desiredY} ${desiredW} ${desiredH}`;
176
+ if (this._lastViewbox === newViewBox) return;
177
+
178
+ this._suppressResizeObserver = true;
179
+ try {
180
+ this.svg.svgObject.setAttribute('viewBox', newViewBox);
181
+ } finally {
182
+ // Allow ResizeObserver events after microtask; use timeout to defer
183
+ setTimeout(() => { this._suppressResizeObserver = false; }, 0);
184
+ }
185
+ this._lastViewbox = newViewBox;
186
+ this._lastContentExtents = curExtSignature;
187
+
188
+ // Lock if the growth applied was small; prevents future tiny increments
189
+ if (heightDelta < 2 && widthDelta < 2 && !this._viewboxLocked) {
190
+ this._viewboxLocked = true;
137
191
  }
138
192
  }
139
193
 
@@ -397,6 +451,12 @@ export class omdDisplay {
397
451
 
398
452
  centerNode() {
399
453
  if (!this.node) return;
454
+ if (!this._centerCallCount) this._centerCallCount = 0;
455
+ this._centerCallCount++;
456
+ if (this._centerCallCount > 500) {
457
+ console.warn('omdDisplay: excessive centerNode calls detected; halting further centering to prevent loop');
458
+ return;
459
+ }
400
460
  const containerWidth = this.container.offsetWidth || 0;
401
461
  const containerHeight = this.container.offsetHeight || 0;
402
462
 
@@ -423,7 +483,6 @@ export class omdDisplay {
423
483
  // For step visualizers, use sequenceWidth/Height instead of total dimensions to exclude visualizer elements from autoscale
424
484
  contentWidth = seq.sequenceWidth || seq.width;
425
485
  contentHeight = seq.sequenceHeight || seq.height;
426
- console.log('omdDisplay: Using sequence dimensions for autoscale - sequenceWidth:', seq.sequenceWidth, 'sequenceHeight:', seq.sequenceHeight, 'width:', seq.width, 'height:', seq.height, 'contentWidth:', contentWidth, 'contentHeight:', contentHeight);
427
486
  }
428
487
  if (seq.getCurrentStep) {
429
488
  const step = seq.getCurrentStep();
@@ -433,12 +492,10 @@ export class omdDisplay {
433
492
  const stepHeight = seq.sequenceHeight || step.height;
434
493
  contentWidth = Math.max(contentWidth, stepWidth);
435
494
  contentHeight = Math.max(contentHeight, stepHeight);
436
- console.log('omdDisplay: Considering step dimensions - stepWidth:', stepWidth, 'stepHeight:', stepHeight, 'finalContentWidth:', contentWidth, 'finalContentHeight:', contentHeight);
437
495
  }
438
496
  }
439
497
  }
440
498
  }
441
- console.log('omdDisplay: Final content dimensions for autoscale - width:', contentWidth, 'height:', contentHeight);
442
499
 
443
500
  // Compute scale to keep within bounds
444
501
  let scale = 1;
@@ -458,14 +515,12 @@ export class omdDisplay {
458
515
  if (this.node) {
459
516
  const ctorName = this.node.constructor?.name;
460
517
  hasStepVisualizer = (ctorName === 'omdStepVisualizer') || this.node.type === 'omdStepVisualizer' || (typeof omdStepVisualizer !== 'undefined' && this.node instanceof omdStepVisualizer);
461
- console.log('omdDisplay: Step visualizer detection (node) ctor:', ctorName, 'type:', this.node.type, 'detected:', hasStepVisualizer);
462
518
  }
463
519
 
464
520
  if (hasStepVisualizer) {
465
521
  // Preserve existing scale if already set on node; otherwise lock to 1.
466
522
  const existingScale = (this.node && typeof this.node.scale === 'number') ? this.node.scale : undefined;
467
523
  scale = (existingScale && existingScale > 0) ? existingScale : 1;
468
- console.log('omdDisplay: Step visualizer detected - locking scale at', scale);
469
524
  } else {
470
525
  const hPad = this.options.edgePadding || 0;
471
526
  const vPadTop = this.options.topMargin || 0;
@@ -483,7 +538,6 @@ export class omdDisplay {
483
538
  const maxScale = (typeof this.options.maxScale === 'number') ? this.options.maxScale : 1;
484
539
  scale = Math.min(sx, sy, maxScale);
485
540
  if (!isFinite(scale) || scale <= 0) scale = 1;
486
- console.log('omdDisplay: Autoscale calculation - availW:', availW, 'availH:', availH, 'contentW:', contentWidth, 'contentH:', contentHeight, 'scale:', scale);
487
541
  }
488
542
  }
489
543
 
@@ -511,6 +565,14 @@ export class omdDisplay {
511
565
  const willOverflowHoriz = scaledWidthFinal > containerWidth;
512
566
  const willOverflowVert = totalNeededH > containerHeight;
513
567
 
568
+ // Avoid looping if content dimension signature hasn't changed
569
+ const contentSig = `${contentWidth}x${contentHeight}x${scale}`;
570
+ if (this._lastCenterSignature === contentSig && !willOverflowHoriz && !willOverflowVert) {
571
+ // Only update position; skip expensive ensureViewboxFits
572
+ if (this.node.setPosition) this.node.setPosition(x, this.options.topMargin);
573
+ return;
574
+ }
575
+
514
576
  if (willOverflowHoriz || willOverflowVert) {
515
577
  // Set scale but do NOT reposition node (preserve its absolute positions).
516
578
  if (this.node.setScale) this.node.setScale(scale);
@@ -546,6 +608,8 @@ export class omdDisplay {
546
608
  }
547
609
  if (this.options.debugExtents) this._drawDebugOverlays();
548
610
  }
611
+
612
+ this._lastCenterSignature = contentSig;
549
613
  }
550
614
 
551
615
  fitToContent() {
@@ -880,4 +944,12 @@ export class omdDisplay {
880
944
  node.positionToolbarOverlay(containerWidth, containerHeight, padding);
881
945
  }
882
946
  }
947
+
948
+ /**
949
+ * Public API: returns the currently rendered root node (could be a step visualizer, sequence, or plain node)
950
+ * @returns {object|null}
951
+ */
952
+ getCurrentNode() {
953
+ return this.node;
954
+ }
883
955
  }
@@ -86,7 +86,6 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
86
86
  this.textBoxManager.clearAllTextBoxes();
87
87
  }
88
88
  const after = this.textBoxManager?.stepTextBoxes?.length || 0;
89
- if (before !== after) console.log('omdStepVisualizer: closeActiveDot removed text boxes', before, '->', after);
90
89
  } catch (e) { /* no-op */ }
91
90
  }
92
91
 
@@ -136,7 +135,7 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
136
135
  // Text boxes
137
136
  textBoxOptions: {
138
137
  backgroundColor: omdColor.white,
139
- borderColor: omdColor.lightGray,
138
+ borderColor: 'none',
140
139
  borderWidth: 1,
141
140
  borderRadius: 5,
142
141
  padding: 8, // Minimal padding for tight fit
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { omdPopup } from './omdPopup.js';
10
- import { jsvgGroup } from '@teachinglab/jsvg';
10
+ import { jsvgGroup, jsvgRect } from '@teachinglab/jsvg';
11
11
 
12
12
  /**
13
13
  * A flexible overlay system for covering nodes with customizable elements
@@ -1,4 +1,4 @@
1
- import { jsvgGroup, jsvgRect, jsvgButton, jsvgTextBox } from '@teachinglab/jsvg';
1
+ import { jsvgGroup, jsvgRect, jsvgButton, jsvgLayoutGroup, jsvgTextInput } from '@teachinglab/jsvg';
2
2
  /**
3
3
  * omdPopup - Handles popup creation and management for node overlays
4
4
  */
@@ -9,7 +9,8 @@ export class omdTranscriptionService {
9
9
  constructor(options = {}) {
10
10
  this.options = {
11
11
  endpoint: options.endpoint || this._getDefaultEndpoint(),
12
- defaultProvider: options.defaultProvider || 'gemini',
12
+ defaultProvider: options.defaultProvider || 'openai',
13
+ defaultModel: options.defaultModel || 'gpt-4o-mini',
13
14
  ...options
14
15
  };
15
16
  }
@@ -60,7 +61,9 @@ export class omdTranscriptionService {
60
61
  },
61
62
  body: JSON.stringify({
62
63
  imageBase64: base64Image,
63
- prompt: options.prompt || 'Please transcribe the handwritten mathematical expression in this image. Return only the mathematical expression in pure format (no LaTeX, no dollar signs). For powers use ^, for fractions use /, for square roots use sqrt().'
64
+ prompt: options.prompt || 'Please transcribe the handwritten mathematical expression in this image. Return only the mathematical expression in pure format (no LaTeX, no dollar signs). For powers use ^, for fractions use /, for square roots use sqrt().',
65
+ model: options.model || this.options.defaultModel,
66
+ provider: options.provider || this.options.defaultProvider
64
67
  })
65
68
  });
66
69
 
@@ -72,8 +75,9 @@ export class omdTranscriptionService {
72
75
 
73
76
 
74
77
  return {
75
- text: result.text,
76
- provider: result.provider || 'gemini',
78
+ text: (result.text || '').trim(),
79
+ provider: result.provider || this.options.defaultProvider,
80
+ model: result.model || (options.model || this.options.defaultModel),
77
81
  confidence: result.confidence || 1.0
78
82
  };
79
83
 
@@ -106,7 +110,7 @@ export class omdTranscriptionService {
106
110
  * @returns {Array} List of available providers
107
111
  */
108
112
  getAvailableProviders() {
109
- return ['gemini']; // Server handles the provider selection
113
+ return ['openai']; // Server handles actual model execution
110
114
  }
111
115
 
112
116
  /**
@@ -115,6 +119,6 @@ export class omdTranscriptionService {
115
119
  * @returns {boolean} True if provider is available
116
120
  */
117
121
  isProviderAvailable(provider) {
118
- return provider === 'gemini'; // Only gemini is supported via server
122
+ return provider === 'openai';
119
123
  }
120
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -36,11 +36,14 @@
36
36
  "express": "^4.18.2",
37
37
  "mathjs": "^14.5.2",
38
38
  "openai": "^4.28.0",
39
- "@teachinglab/jsvg": "0.1.1"
39
+ "@teachinglab/jsvg": "0.1.1",
40
+ "@netlify/vite-plugin": "^2.5.4",
41
+ "vite": "^5.4.0"
40
42
  },
41
43
  "scripts": {
42
- "start": "node --env-file=.env server.js",
43
- "dev": "vite"
44
+ "dev": "vite",
45
+ "build": "vite build",
46
+ "dev:netlify": "netlify dev"
44
47
  },
45
48
  "publishConfig": {
46
49
  "access": "public"
package/src/omdTable.js CHANGED
@@ -34,7 +34,7 @@ export class omdTable extends jsvgGroup
34
34
  this.showBackground = true;
35
35
 
36
36
  // Alternating row color properties
37
- this.alternatingRowColors = null; // Should be an array of colors or null
37
+ this.alternatingRowColors = [omdColor.mediumGray, omdColor.lightGray]; // Should be an array of colors or null
38
38
  this.headerBackgroundColor = omdColor.lightGray;
39
39
  this.cellBackgroundColor = "white";
40
40