munchboka-edutools 0.1.13__py3-none-any.whl

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.

Potentially problematic release.


This version of munchboka-edutools might be problematic. Click here for more details.

Files changed (149) hide show
  1. munchboka_edutools/__init__.py +184 -0
  2. munchboka_edutools/_plotmath_shim.py +126 -0
  3. munchboka_edutools/_version.py +2 -0
  4. munchboka_edutools/directives/__init__.py +1 -0
  5. munchboka_edutools/directives/admonitions.py +389 -0
  6. munchboka_edutools/directives/cas_popup.py +272 -0
  7. munchboka_edutools/directives/clear.py +103 -0
  8. munchboka_edutools/directives/dialogue.py +137 -0
  9. munchboka_edutools/directives/escape_room.py +296 -0
  10. munchboka_edutools/directives/factor_tree.py +549 -0
  11. munchboka_edutools/directives/ggb.py +209 -0
  12. munchboka_edutools/directives/ggb_icon.py +105 -0
  13. munchboka_edutools/directives/ggb_popup.py +165 -0
  14. munchboka_edutools/directives/horner.py +324 -0
  15. munchboka_edutools/directives/interactive_code.py +75 -0
  16. munchboka_edutools/directives/jeopardy.py +252 -0
  17. munchboka_edutools/directives/multi_plot.py +1126 -0
  18. munchboka_edutools/directives/pair_puzzle.py +191 -0
  19. munchboka_edutools/directives/parsons.py +109 -0
  20. munchboka_edutools/directives/plot.py +3105 -0
  21. munchboka_edutools/directives/poly_icon.py +111 -0
  22. munchboka_edutools/directives/polydiv.py +344 -0
  23. munchboka_edutools/directives/popup.py +245 -0
  24. munchboka_edutools/directives/quiz.py +291 -0
  25. munchboka_edutools/directives/signchart.py +516 -0
  26. munchboka_edutools/directives/timed_quiz.py +436 -0
  27. munchboka_edutools/directives/turtle.py +157 -0
  28. munchboka_edutools/static/css/admonitions.css +2012 -0
  29. munchboka_edutools/static/css/cas_popup.css +242 -0
  30. munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
  31. munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
  32. munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
  33. munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
  34. munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
  35. munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
  36. munchboka_edutools/static/css/dialogue.css +92 -0
  37. munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
  38. munchboka_edutools/static/css/figures.css +274 -0
  39. munchboka_edutools/static/css/general_style.css +74 -0
  40. munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
  41. munchboka_edutools/static/css/github-dark.css +112 -0
  42. munchboka_edutools/static/css/github-light.css +120 -0
  43. munchboka_edutools/static/css/interactive_code/style.css +575 -0
  44. munchboka_edutools/static/css/interactive_code.css +582 -0
  45. munchboka_edutools/static/css/jeopardy.css +529 -0
  46. munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
  47. munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
  48. munchboka_edutools/static/css/popup.css +115 -0
  49. munchboka_edutools/static/css/quiz.css +312 -0
  50. munchboka_edutools/static/css/timedQuiz.css +375 -0
  51. munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
  52. munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
  53. munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
  54. munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
  55. munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
  56. munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
  57. munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
  58. munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
  59. munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
  60. munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
  61. munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
  62. munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
  63. munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
  64. munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
  65. munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
  66. munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
  67. munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
  68. munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
  69. munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
  70. munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
  71. munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
  72. munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
  73. munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
  74. munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
  75. munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
  76. munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
  77. munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
  78. munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
  79. munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
  80. munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
  81. munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
  82. munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
  83. munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
  84. munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
  85. munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
  86. munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
  87. munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
  88. munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
  89. munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
  90. munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
  91. munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
  92. munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
  93. munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
  94. munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
  95. munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
  96. munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
  97. munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
  98. munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
  99. munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
  100. munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
  101. munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
  102. munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
  103. munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
  104. munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
  105. munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
  106. munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
  107. munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
  108. munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
  109. munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
  110. munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
  111. munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
  112. munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
  113. munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
  114. munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
  115. munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
  116. munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
  117. munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
  118. munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
  119. munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
  120. munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
  121. munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
  122. munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
  123. munchboka_edutools/static/icons/stickers/edit.svg +1 -0
  124. munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
  125. munchboka_edutools/static/js/casThemeManager.js +99 -0
  126. munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
  127. munchboka_edutools/static/js/geogebra-setup.js +6 -0
  128. munchboka_edutools/static/js/highlight-init.js +6 -0
  129. munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
  130. munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
  131. munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
  132. munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
  133. munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
  134. munchboka_edutools/static/js/jeopardy.js +523 -0
  135. munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
  136. munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
  137. munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
  138. munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
  139. munchboka_edutools/static/js/popup.js +85 -0
  140. munchboka_edutools/static/js/quiz.js +422 -0
  141. munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
  142. munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
  143. munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
  144. munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
  145. munchboka_edutools/static/js/utils.js +3 -0
  146. munchboka_edutools-0.1.13.dist-info/METADATA +108 -0
  147. munchboka_edutools-0.1.13.dist-info/RECORD +149 -0
  148. munchboka_edutools-0.1.13.dist-info/WHEEL +4 -0
  149. munchboka_edutools-0.1.13.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,203 @@
1
+ function generateUUID() {
2
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
3
+ const r = (Math.random() * 16) | 0;
4
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
5
+ return v.toString(16);
6
+ });
7
+ }
8
+
9
+
10
+ class TurtleCode {
11
+ /**
12
+ * @param {string} containerId - ID of an existing <div> that will hold this environment.
13
+ * @param {string} [initialCode=""] - Initial Python code to display in the editor.
14
+ * @param {Object} [cmOptions={}] - Additional CodeMirror options to merge in.
15
+ */
16
+ constructor(containerId, initialCode = "", cmOptions = {}) {
17
+ this.containerId = containerId;
18
+ this.container = document.getElementById(containerId);
19
+ if (!this.container) {
20
+ throw new Error(`Container with id="${containerId}" not found.`);
21
+ }
22
+ this.initialCode = initialCode;
23
+ this.cmOptions = cmOptions;
24
+
25
+ this.uniqueSuffix = generateUUID(); // to avoid ID collisions
26
+
27
+ this.createUI();
28
+
29
+ // Initialize your existing CodeEditor on the created <textarea>
30
+ this.editor = new CodeEditor(this.textAreaEl.id);
31
+
32
+ // Add a small delay before setting the initial code
33
+ setTimeout(() => {
34
+ this.editor.setValue(this.initialCode);
35
+ }, 300); // 100ms delay should be enough
36
+ // this.editor.setValue(this.initialCode);
37
+
38
+ this.runButtonEl.addEventListener("click", () => this.runCode());
39
+ }
40
+
41
+ createUI() {
42
+ // Clear any existing content in the container
43
+ this.container.innerHTML = "";
44
+
45
+ // A flex container to hold the editor (left) and the turtle div (right)
46
+ const editorAndCanvasContainer = document.createElement("div");
47
+
48
+ editorAndCanvasContainer.classList.add("turtle-env"); // for styling
49
+
50
+ editorAndCanvasContainer.style.display = "flex";
51
+ editorAndCanvasContainer.style.flexWrap = "wrap"; // in case of narrow screens
52
+ editorAndCanvasContainer.style.width = "100%";
53
+
54
+ // --- LEFT COLUMN: Editor, Run Button, Output ---
55
+ const editorContainer = document.createElement("div");
56
+
57
+ editorContainer.classList.add("turtle-left"); // for styling
58
+
59
+ editorContainer.style.flex = "1"; // Take 50% of available width
60
+ editorContainer.style.minWidth = "100px"; // Reasonable min width
61
+ editorContainer.style.display = "flex";
62
+ editorContainer.style.flexDirection = "column";
63
+
64
+ // Textarea for CodeMirror
65
+ this.textAreaEl = document.createElement("textarea");
66
+ this.textAreaEl.id = `skulpt-editor-${this.uniqueSuffix}`;
67
+ // Hide the raw <textarea> so CodeMirror can replace it
68
+ this.textAreaEl.style.display = "none";
69
+ editorContainer.appendChild(this.textAreaEl);
70
+
71
+ // Run Button
72
+ this.runButtonEl = document.createElement("button");
73
+ this.runButtonEl.className = "button button-run";
74
+ this.runButtonEl.id = `skulpt-run-btn-${this.uniqueSuffix}`;
75
+ this.runButtonEl.textContent = "Kjør kode";
76
+ this.runButtonEl.style.margin = "1em 0";
77
+ editorContainer.appendChild(this.runButtonEl);
78
+
79
+ // Output <pre>
80
+ this.outputEl = document.createElement("pre");
81
+ this.outputEl.className = "pythonoutput";
82
+ this.outputEl.id = `skulpt-output-${this.uniqueSuffix}`;
83
+ this.outputEl.style.border = "1px solid #ccc";
84
+ this.outputEl.style.padding = "8px";
85
+ this.outputEl.style.minHeight = "2em";
86
+ this.outputEl.style.whiteSpace = "pre-wrap";
87
+ editorContainer.appendChild(this.outputEl);
88
+
89
+ editorAndCanvasContainer.appendChild(editorContainer);
90
+
91
+ // --- RIGHT COLUMN: Turtle Div ---
92
+ const canvasContainer = document.createElement("div");
93
+
94
+ // canvasContainer.classList.add("turtle-right"); // for styling
95
+
96
+ canvasContainer.style.flex = "1"; // Take the other 50%
97
+ canvasContainer.style.minWidth = "350px"; // Reasonable min width
98
+ canvasContainer.style.display = "flex";
99
+ canvasContainer.style.flexDirection = "column";
100
+ canvasContainer.style.alignItems = "center";
101
+ canvasContainer.style.justifyContent = "center";
102
+
103
+ // Turtle div
104
+ this.canvasEl = document.createElement("div");
105
+ this.canvasEl.id = `skulpt-canvas-${this.uniqueSuffix}`;
106
+ this.canvasEl.style.border = "1px solid #ccc";
107
+ this.canvasEl.style.width = "95%"; // Adjust as needed
108
+ this.canvasEl.style.height = "400px";
109
+ this.canvasEl.style.marginTop = "0em";
110
+ this.canvasEl.style.boxSizing = "border-box";
111
+ canvasContainer.appendChild(this.canvasEl);
112
+
113
+ editorAndCanvasContainer.appendChild(canvasContainer);
114
+
115
+ // Finally, append the flex container into the main container
116
+ this.container.appendChild(editorAndCanvasContainer);
117
+ }
118
+
119
+ runCode() {
120
+ let userCode = this.editor.getValue();
121
+ this.outputEl.innerHTML = ""; // clear output
122
+
123
+
124
+ const mode = document.documentElement.getAttribute("data-mode");
125
+ // default to black
126
+ let forcedColor = "black";
127
+ if (mode === "dark") {
128
+ forcedColor = "white";
129
+ }
130
+ else if (mode === "light") {
131
+ forcedColor = "black";
132
+ }
133
+ else if (mode === "auto") {
134
+ const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
135
+ forcedColor = prefersDarkScheme ? "white" : "black";
136
+ }
137
+
138
+ console.log("Mode", mode);
139
+
140
+
141
+ // Safely prepend a snippet that ensures turtle is white
142
+ // We also do a basic check to ensure turtle is imported before coloring
143
+ // (some users might not have "import turtle" at all).
144
+ // "import turtle" will just re-import gracefully if the user already had it.
145
+ const snippet = `
146
+ try:
147
+ import turtle
148
+ turtle.color("${forcedColor}")
149
+ screen = turtle.Screen()
150
+
151
+ # screen.setup(width=300, height=300)
152
+
153
+ # Set a coordinate system so the center of the visible area is (0,0).
154
+ # For example, left = -200, right = 200, bottom = -150, top = 150
155
+ # screen.setworldcoordinates(-200, -150, 200, 150)
156
+ except:
157
+ pass
158
+
159
+ `;
160
+ userCode = snippet + userCode;
161
+
162
+ // Output function for Python's print
163
+ const outf = (text) => {
164
+ this.outputEl.innerHTML += text;
165
+ };
166
+
167
+ // Helper for Skulpt to read built-in libs
168
+ const builtinRead = (filename) => {
169
+ if (!Sk.builtinFiles || !Sk.builtinFiles["files"][filename]) {
170
+ throw new Error("File not found: " + filename);
171
+ }
172
+ return Sk.builtinFiles["files"][filename];
173
+ };
174
+
175
+ // Configure Skulpt
176
+ Sk.pre = this.outputEl.id;
177
+ Sk.configure({
178
+ output: outf,
179
+ read: builtinRead,
180
+ python3: true,
181
+ });
182
+
183
+ // Point turtle to our <div>
184
+ (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = this.canvasEl.id;
185
+
186
+ // Asynchronously run the code
187
+ Sk.misceval.asyncToPromise(() => {
188
+ return Sk.importMainWithBody("<stdin>", false, userCode, true);
189
+ }).then(
190
+ () => {
191
+ // success
192
+ },
193
+ (err) => {
194
+ // error
195
+ this.outputEl.innerHTML = err.toString();
196
+ }
197
+ );
198
+ }
199
+ }
200
+
201
+ function makeTurtleCode(containerId, initialCode = "", cmOptions = {}) {
202
+ return new TurtleCode(containerId, initialCode, cmOptions);
203
+ }
@@ -0,0 +1,353 @@
1
+ // workerManager.js
2
+
3
+ class WorkerManager {
4
+ static instance = null;
5
+
6
+ static getInstance(preloadPackages = null) {
7
+ if (!WorkerManager.instance) {
8
+ // Default PYODIDE packages (micropip is needed for custom packages)
9
+ const defaultPreloadPackages = ['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip'];
10
+ const combinedPreloadPackages = Array.from(new Set(preloadPackages ? [...defaultPreloadPackages, ...preloadPackages] : defaultPreloadPackages));
11
+ WorkerManager.instance = new WorkerManager(combinedPreloadPackages);
12
+
13
+ // Trigger warm-up after initialization
14
+ setTimeout(() => {
15
+ WorkerManager.instance.warmUpPyodide().catch(err => {
16
+ console.warn("Pyodide warm-up failed:", err);
17
+ });
18
+ }, 1000); // Small delay to let the page finish loading
19
+
20
+
21
+
22
+ } else if (preloadPackages) {
23
+ // Only load packages that are NOT already loaded.
24
+ const packagesToLoad = preloadPackages.filter(pkg => !WorkerManager.instance.loadedPackages.has(pkg));
25
+ const combinedPreloadPackages = Array.from(new Set(['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip', ...packagesToLoad]));
26
+ if (combinedPreloadPackages.length > 0) {
27
+ WorkerManager.instance.loadPackages(combinedPreloadPackages);
28
+ }
29
+ }
30
+ return WorkerManager.instance;
31
+ }
32
+
33
+ constructor(preloadPackages = []) { // Corrected default
34
+ if (WorkerManager.instance) {
35
+ return WorkerManager.instance;
36
+ }
37
+
38
+ this.worker = null;
39
+ this.callbacks = {};
40
+ this.preloadPackages = preloadPackages;
41
+ this.loadedPackages = new Set();
42
+ this.packageLoadPromises = {};
43
+ console.log("Preload packages in WorkerManager:", this.preloadPackages);
44
+
45
+ this.workerReadyPromise = new Promise((resolve, reject) => {
46
+ this.workerReadyResolve = resolve;
47
+ this.workerReadyReject = reject;
48
+ });
49
+
50
+ this.initWorker();
51
+
52
+ WorkerManager.instance = this;
53
+
54
+
55
+ }
56
+
57
+ initWorker() {
58
+ const workerScript = `
59
+ importScripts('https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js');
60
+
61
+ let pyodideReadyPromise = loadPyodide();
62
+ let initialGlobals = new Set();
63
+
64
+ async function resetPyodide(pyodide, initialGlobals) {
65
+ const currentGlobals = new Set(pyodide.globals.keys());
66
+ const globalsToClear = Array.from(currentGlobals).filter(x => !initialGlobals.has(x));
67
+ for (const key of globalsToClear) {
68
+ pyodide.globals.delete(key);
69
+ }
70
+ console.log("Globals cleared:", globalsToClear);
71
+ }
72
+
73
+ // Helper function to install packages via micropip
74
+ async function installPackages(pyodide, packages) {
75
+ if (packages.length > 0) {
76
+ await pyodide.loadPackage("micropip"); // Load micropip
77
+ const micropip = pyodide.pyimport("micropip");
78
+ await micropip.install(packages);
79
+ }
80
+ }
81
+
82
+ onmessage = async (event) => {
83
+ const messageId = event.data.messageId;
84
+
85
+ if (event.data.type === 'init') {
86
+ const { preloadPackages } = event.data; // Receive preloadPackages
87
+ const pyodide = await pyodideReadyPromise;
88
+ initialGlobals = new Set(pyodide.globals.keys());
89
+
90
+ // Separate Pyodide packages from PyPI packages
91
+ const pyodidePackages = preloadPackages.filter(pkg => ['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip'].includes(pkg));
92
+ const pypiPackages = preloadPackages.filter(pkg => !['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip'].includes(pkg));
93
+
94
+ // Load Pyodide packages
95
+ if (pyodidePackages.length > 0) {
96
+ await pyodide.loadPackage(pyodidePackages);
97
+ }
98
+
99
+ // Install PyPI packages using micropip
100
+ await installPackages(pyodide, pypiPackages);
101
+
102
+ postMessage(JSON.stringify({ type: 'initReady' })); // Send AFTER preloading
103
+ }
104
+
105
+ if (event.data.type === 'runCode') {
106
+ const { code } = event.data;
107
+ try {
108
+ const pyodide = await pyodideReadyPromise;
109
+ await resetPyodide(pyodide, initialGlobals);
110
+
111
+ // Prepare the Python code
112
+ const pyCode = \`
113
+ import sys
114
+ import json
115
+ import io
116
+ import base64
117
+ from js import postMessage
118
+ import matplotlib
119
+ matplotlib.use('Agg')
120
+ import matplotlib.pyplot as plt
121
+
122
+ class PyConsole:
123
+ def __init__(self, messageId):
124
+ self.messageId = messageId
125
+ self.buffer = ""
126
+
127
+ def write(self, msg):
128
+ self.buffer += msg
129
+ # Flush immediately for any output
130
+ self.flush()
131
+
132
+ def flush(self):
133
+ if self.buffer:
134
+ postMessage(json.dumps({'type': 'stdout', 'msg': self.buffer, 'messageId': self.messageId}))
135
+ self.buffer = ""
136
+
137
+ sys.stdout = PyConsole("\${messageId}")
138
+ sys.stderr = PyConsole("\${messageId}")
139
+
140
+ # Override plt.show()
141
+ def show_override(messageId):
142
+ buf = io.BytesIO()
143
+ plt.savefig(buf, format='png')
144
+ fig = plt.gcf()
145
+ width_in = fig.get_figwidth()
146
+ height_in = fig.get_figheight()
147
+ dpi = fig.get_dpi()
148
+ width_px = int(width_in * dpi)
149
+ height_px = int(height_in * dpi)
150
+ buf.seek(0)
151
+ image_base64 = base64.b64encode(buf.read()).decode('utf-8')
152
+ postMessage(json.dumps({
153
+ 'type': 'plot',
154
+ 'data': image_base64,
155
+ 'messageId': messageId,
156
+ 'width': width_px,
157
+ 'height': height_px
158
+ }))
159
+ plt.clf()
160
+ # Add a newline after the plot
161
+ sys.stdout.write('\\\\\\\\n')
162
+ sys.stdout.write('\\\\\\\\n')
163
+ sys.stdout.flush()
164
+
165
+ plt.show = lambda: show_override("\${messageId}")
166
+ \`;
167
+
168
+
169
+ await pyodide.runPythonAsync(pyCode);
170
+ await pyodide.runPythonAsync(code);
171
+ postMessage(JSON.stringify({ type: 'executionComplete', messageId }));
172
+ } catch (err) {
173
+ postMessage(JSON.stringify({ type: 'stderr', msg: String(err), messageId }));
174
+ }
175
+ }
176
+
177
+ if (event.data.type === 'loadPackage') {
178
+ const { packages, packageRequestId } = event.data;
179
+ try {
180
+ const pyodide = await pyodideReadyPromise;
181
+ console.log("Loading packages:", packages);
182
+
183
+ // Separate Pyodide packages from PyPI packages
184
+ const pyodidePackages = packages.filter(pkg => ['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip'].includes(pkg));
185
+ const pypiPackages = packages.filter(pkg => !['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip'].includes(pkg));
186
+
187
+ // Load Pyodide packages directly
188
+ if (pyodidePackages.length > 0) {
189
+ await pyodide.loadPackage(pyodidePackages);
190
+ }
191
+
192
+ // Install custom packages using micropip
193
+ await installPackages(pyodide, pypiPackages);
194
+
195
+ console.log("Packages loaded:", packages);
196
+ postMessage(JSON.stringify({ type: 'packagesLoaded', packageRequestId }));
197
+ } catch (err) {
198
+ postMessage(JSON.stringify({ type: 'stderr', msg: String(err), packageRequestId }));
199
+ }
200
+ }
201
+ };
202
+ `;
203
+
204
+ const workerBlob = new Blob([workerScript], { type: 'application/javascript' });
205
+ this.worker = new Worker(URL.createObjectURL(workerBlob));
206
+
207
+ this.worker.onmessage = this.handleMessage.bind(this);
208
+ this.worker.onerror = this.handleError.bind(this);
209
+
210
+ // Send preloadPackages with the init message!
211
+ this.worker.postMessage({ type: 'init', preloadPackages: this.preloadPackages });
212
+
213
+
214
+ this.pyodideWarmedUp = false;
215
+ }
216
+
217
+ warmUpPyodide() {
218
+ // Only warm up once
219
+ if (this.pyodideWarmedUp) {
220
+ return Promise.resolve();
221
+ }
222
+
223
+ return this.workerReadyPromise.then(() => {
224
+ console.log("Warming up Pyodide with empty execution...");
225
+ return new Promise((resolve) => {
226
+ const messageId = this.generateMessageId();
227
+ this.callbacks[messageId] = (data) => {
228
+ if (data.type === 'executionComplete') {
229
+ console.log("Pyodide warm-up complete");
230
+ this.pyodideWarmedUp = true;
231
+ resolve();
232
+ }
233
+ };
234
+ this.worker.postMessage({
235
+ type: 'runCode',
236
+ code: '# Empty script to trigger Pyodide compilation',
237
+ messageId
238
+ });
239
+ });
240
+ });
241
+ }
242
+
243
+ generateMessageId() {
244
+ return 'msg-' + Math.random().toString(36).substr(2, 9);
245
+ }
246
+
247
+ loadPackages(packages) {
248
+ const packagesToLoad = packages.filter(pkg => !this.loadedPackages.has(pkg));
249
+
250
+ if (packagesToLoad.length === 0) {
251
+ return Promise.resolve(); // All packages already loaded
252
+ }
253
+
254
+ const packageRequestId = 'pkg-' + Math.random().toString(36).substr(2, 9);
255
+
256
+ return new Promise((resolve, reject) => {
257
+ this.packageLoadPromises[packageRequestId] = { resolve, reject, packages: packagesToLoad };
258
+ this.worker.postMessage({ type: 'loadPackage', packages: packagesToLoad, packageRequestId });
259
+ });
260
+ }
261
+
262
+ runCode(code, onMessageCallback) {
263
+ const messageId = this.generateMessageId();
264
+ this.callbacks[messageId] = onMessageCallback;
265
+ this.worker.postMessage({ type: 'runCode', code, messageId });
266
+ return messageId;
267
+ }
268
+
269
+ handleMessage(event) {
270
+ let data;
271
+ try {
272
+ data = JSON.parse(event.data);
273
+ } catch (e) {
274
+ console.error("Failed to parse message from worker:", event.data);
275
+ return;
276
+ }
277
+
278
+ const messageId = data.messageId;
279
+ const packageRequestId = data.packageRequestId;
280
+
281
+ if (messageId && this.callbacks[messageId]) {
282
+ this.callbacks[messageId](data);
283
+
284
+ if (data.type === 'executionComplete') {
285
+ delete this.callbacks[messageId];
286
+ }
287
+ } else if (packageRequestId && this.packageLoadPromises[packageRequestId]) {
288
+ const packagePromise = this.packageLoadPromises[packageRequestId];
289
+ if (data.type === 'packagesLoaded') {
290
+ for (const pkg of packagePromise.packages) {
291
+ this.loadedPackages.add(pkg);
292
+ }
293
+ packagePromise.resolve();
294
+ } else if (data.type === 'stderr') {
295
+ packagePromise.reject(new Error(data.msg));
296
+ }
297
+ delete this.packageLoadPromises[packageRequestId];
298
+ } else {
299
+ if (data.type === 'initReady') {
300
+ console.log("Worker initialization message:", data.type);
301
+ this.workerReadyResolve(); // Resolve *after* preloading is done in the worker.
302
+ } else {
303
+ console.warn("Unhandled message from worker:", data);
304
+ }
305
+ }
306
+ }
307
+
308
+ handleError(error) {
309
+ console.error("Worker error:", error);
310
+ if (this.workerReadyReject) {
311
+ this.workerReadyReject(error);
312
+ }
313
+ }
314
+
315
+ // restartWorker() {
316
+ // if (this.worker) {
317
+ // this.worker.terminate();
318
+ // this.worker = null;
319
+ // }
320
+
321
+ // this.loadedPackages = new Set();
322
+ // this.workerReadyPromise = new Promise((resolve, reject) => {
323
+ // this.workerReadyResolve = resolve;
324
+ // this.workerReadyReject = reject;
325
+ // });
326
+
327
+ // this.initWorker();
328
+ // }
329
+
330
+ restartWorker() {
331
+ if (this.worker) {
332
+ this.worker.terminate();
333
+ this.worker = null;
334
+ }
335
+
336
+ this.loadedPackages = new Set();
337
+ this.pyodideWarmedUp = false; // Reset warm-up status
338
+
339
+ this.workerReadyPromise = new Promise((resolve, reject) => {
340
+ this.workerReadyResolve = resolve;
341
+ this.workerReadyReject = reject;
342
+ });
343
+
344
+ this.initWorker();
345
+
346
+ // Trigger warm-up after reinitialization
347
+ setTimeout(() => {
348
+ this.warmUpPyodide().catch(err => {
349
+ console.warn("Pyodide warm-up failed after restart:", err);
350
+ });
351
+ }, 1000); // Small delay to let the worker initialize
352
+ }
353
+ }