@teachinglab/omd 0.1.5 → 0.1.6

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.
package/omd/core/index.js CHANGED
@@ -21,6 +21,7 @@ import { omdEquationStack } from './omdEquationStack.js';
21
21
  import { omdStepVisualizer } from '../step-visualizer/omdStepVisualizer.js';
22
22
  import { omdDisplay } from '../display/omdDisplay.js';
23
23
  import { omdToolbar } from '../display/omdToolbar.js';
24
+ import { omdNodeOverlay, omdNodeOverlayPresets } from '../utils/omdNodeOverlay.js';
24
25
 
25
26
  // Utilities
26
27
  import { getNodeForAST } from './omdUtilities.js';
@@ -30,6 +31,7 @@ import {
30
31
  setConfig,
31
32
  getDefaultConfig
32
33
  } from '../config/omdConfigManager.js';
34
+ import { aiNextEquationStep } from '../utils/aiNextEquationStep.js';
33
35
 
34
36
  // Expression handling
35
37
  import { omdExpression } from '../../src/omdExpression.js';
@@ -60,6 +62,8 @@ export {
60
62
  omdStepVisualizer,
61
63
  omdDisplay,
62
64
  omdToolbar,
65
+ omdNodeOverlay,
66
+ omdNodeOverlayPresets,
63
67
 
64
68
  // Utilities
65
69
  getNodeForAST,
@@ -69,6 +73,8 @@ export {
69
73
  getDefaultConfig,
70
74
  omdExpression,
71
75
  omdColor
76
+ ,
77
+ aiNextEquationStep
72
78
  };
73
79
 
74
80
  // Helper utilities for common operations
@@ -131,6 +137,8 @@ export default {
131
137
  omdStepVisualizer,
132
138
  omdDisplay,
133
139
  omdToolbar,
140
+ omdNodeOverlay,
141
+ omdNodeOverlayPresets,
134
142
 
135
143
  // Utilities
136
144
  getNodeForAST,
@@ -140,6 +148,7 @@ export default {
140
148
  getDefaultConfig,
141
149
  omdExpression,
142
150
  omdColor,
151
+ aiNextEquationStep,
143
152
 
144
153
  // Helper functions
145
154
  helpers: omdHelpers
@@ -479,6 +479,21 @@ export class omdEquationSequenceNode extends omdNode {
479
479
  this.updateLayout();
480
480
  }
481
481
 
482
+ /**
483
+ * Convenience helper: recompute dimensions, update layout, and optionally render via a renderer.
484
+ * Use this instead of calling computeDimensions/updateLayout everywhere.
485
+ * @param {object} [renderer] - Optional renderer (e.g., an omdDisplay instance) to re-render the sequence
486
+ */
487
+ refresh(renderer, center=true) {
488
+ this.computeDimensions();
489
+ this.updateLayout();
490
+ renderer.render(this);
491
+
492
+ if (center) {
493
+ renderer.centerNode();
494
+ }
495
+ }
496
+
482
497
  /**
483
498
  * Applies a specified operation to the current equation in the sequence and adds the result as a new step.
484
499
  * @param {number|string} value - The constant value or expression string to apply.
@@ -0,0 +1,106 @@
1
+ import OpenAI from 'openai';
2
+
3
+ /**
4
+ * Ask the AI for the next algebraic step for an equation.
5
+ * This helper can be called in Node (server) with an API key, or can proxy to an endpoint.
6
+ *
7
+ * @param {string} equation - The equation string (e.g. "3x+2=14")
8
+ * @param {object} [options]
9
+ * @param {string} [options.apiKey] - API key for the OpenAI-compatible client (GEMINI_API_KEY)
10
+ * @param {string} [options.baseURL] - Optional baseURL override for the OpenAI client
11
+ * @param {string} [options.model] - Model name (default: "gemini-2.0-flash")
12
+ * @param {string} [options.endpoint] - If provided, POSTs to this endpoint with { equation } and expects the same JSON response
13
+ */
14
+ export async function aiNextEquationStep(equation, options = {}) {
15
+ if (!equation) throw new Error('equation is required');
16
+
17
+ const prompt = `Given the equation ${equation}, return ONLY the next algebraic step required to solve for the variable, in one of the following strict JSON formats:
18
+
19
+ For an operation (add, subtract, multiply, divide):
20
+ { "operation": "add|subtract|multiply|divide", "value": number or string }
21
+
22
+ For a function applied to both sides (e.g., sqrt, log, sin):
23
+ { "function": "functionName" }
24
+
25
+ If the equation is already solved (like x=4) or no valid algebraic step can be applied:
26
+ { "operation": "none" }
27
+
28
+ Important rules for solving:
29
+ 1. When variables appear on both sides, first move all variables to one side by adding/subtracting variable terms
30
+ 2. When constants appear on both sides, move all constants to one side by adding/subtracting constants
31
+ 3. Focus on isolating the variable systematically
32
+ 4. For equations like 2x-5=3x+7, subtract the smaller variable term (2x) from both sides first
33
+
34
+ Do not include any explanation, LaTeX, or extra text. Only output the JSON object.`;
35
+
36
+ // If an endpoint is provided, proxy the request there (useful for browser usage)
37
+ if (options.endpoint) {
38
+ const res = await fetch(options.endpoint, {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json'
42
+ },
43
+ body: JSON.stringify({ equation })
44
+ });
45
+
46
+ if (!res.ok) {
47
+ const text = await res.text();
48
+ throw new Error(`Remote endpoint error: ${res.status} ${text}`);
49
+ }
50
+
51
+ const json = await res.json();
52
+ return json;
53
+ }
54
+
55
+ const apiKey = options.apiKey || (typeof process !== 'undefined' && process.env && process.env.GEMINI_API_KEY);
56
+ if (!apiKey) throw new Error('API key is required when not using a remote endpoint');
57
+
58
+ const baseURL = options.baseURL || 'https://generativelanguage.googleapis.com/v1beta/openai/';
59
+ const model = options.model || 'gemini-2.0-flash';
60
+
61
+ const client = new OpenAI({ apiKey, baseURL });
62
+
63
+ const messages = [
64
+ {
65
+ role: 'user',
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: prompt
70
+ }
71
+ ]
72
+ }
73
+ ];
74
+
75
+ const response = await client.chat.completions.create({
76
+ model,
77
+ messages,
78
+ max_tokens: 150
79
+ });
80
+
81
+ // Extract JSON object from model output
82
+ let aiText = '';
83
+ try {
84
+ aiText = (response.choices && response.choices[0] && response.choices[0].message && response.choices[0].message.content) || '';
85
+ if (Array.isArray(aiText)) {
86
+ // Some SDKs return arrays of content objects
87
+ aiText = aiText.map(c => c.text || c.content || '').join('');
88
+ }
89
+ if (typeof aiText === 'object' && aiText.text) aiText = aiText.text;
90
+ aiText = String(aiText).trim();
91
+ } catch (e) {
92
+ throw new Error('Unexpected AI response format');
93
+ }
94
+
95
+ const jsonMatch = aiText.match(/\{[\s\S]*\}/);
96
+ if (!jsonMatch) throw new Error('AI did not return a JSON object');
97
+
98
+ try {
99
+ const parsed = JSON.parse(jsonMatch[0]);
100
+ return parsed;
101
+ } catch (e) {
102
+ throw new Error('Failed to parse AI JSON response: ' + e.message + ' -- raw: ' + aiText);
103
+ }
104
+ }
105
+
106
+ export default aiNextEquationStep;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { omdColor } from "./omdColor.js";
3
- import { jsvgGroup, jsvgLine, jsvgEllipse, jsvgRect } from "@teachinglab/jsvg";
3
+ import { jsvgGroup, jsvgLine, jsvgEllipse, jsvgRect, jsvgTextBox } from "@teachinglab/jsvg";
4
4
 
5
5
  export class omdBalanceHanger extends jsvgGroup
6
6
  {
@@ -1,5 +1,5 @@
1
1
  import { omdColor } from "./omdColor.js";
2
- import { jsvgGroup, jsvgRect, jsvgClipMask, jsvgLine, jsvgEllipse, jsvgTextBox, jsvgPath } from "@teachinglab/jsvg";
2
+ import { jsvgGroup, jsvgRect, jsvgClipMask, jsvgLine, jsvgEllipse, jsvgTextBox, jsvgTextLine, jsvgPath } from "@teachinglab/jsvg";
3
3
  import {
4
4
  omdRightTriangle,
5
5
  omdIsoscelesTriangle,