munchboka-edutools 0.1.0__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.
- munchboka_edutools/__init__.py +182 -0
- munchboka_edutools/_plotmath_shim.py +126 -0
- munchboka_edutools/_version.py +2 -0
- munchboka_edutools/directives/__init__.py +1 -0
- munchboka_edutools/directives/admonitions.py +356 -0
- munchboka_edutools/directives/cas_popup.py +272 -0
- munchboka_edutools/directives/dialogue.py +137 -0
- munchboka_edutools/directives/escape_room.py +296 -0
- munchboka_edutools/directives/factor_tree.py +549 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +62 -0
- munchboka_edutools/directives/ggb_popup.py +165 -0
- munchboka_edutools/directives/horner.py +324 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/multi_plot.py +1126 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3012 -0
- munchboka_edutools/directives/poly_icon.py +91 -0
- munchboka_edutools/directives/polydiv.py +344 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/signchart.py +474 -0
- munchboka_edutools/directives/timed_quiz.py +436 -0
- munchboka_edutools/directives/turtle.py +157 -0
- munchboka_edutools/static/css/admonitions.css +2012 -0
- munchboka_edutools/static/css/cas_popup.css +242 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
- munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
- munchboka_edutools/static/css/dialogue.css +92 -0
- munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
- munchboka_edutools/static/css/figures.css +274 -0
- munchboka_edutools/static/css/general_style.css +74 -0
- munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
- munchboka_edutools/static/css/github-dark.css +112 -0
- munchboka_edutools/static/css/github-light.css +120 -0
- munchboka_edutools/static/css/interactive_code/style.css +575 -0
- munchboka_edutools/static/css/interactive_code.css +582 -0
- munchboka_edutools/static/css/jeopardy.css +476 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
- munchboka_edutools/static/css/quiz.css +312 -0
- munchboka_edutools/static/css/timedQuiz.css +375 -0
- munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
- munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
- munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/stickers/edit.svg +1 -0
- munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
- munchboka_edutools/static/js/casThemeManager.js +99 -0
- munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
- munchboka_edutools/static/js/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
- munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
- munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
- munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
- munchboka_edutools/static/js/interactive_code/codeEditor.js +662 -0
- munchboka_edutools/static/js/interactive_code/interactiveCodeSetup.js +252 -0
- munchboka_edutools/static/js/interactive_code/pythonRunner.js +145 -0
- munchboka_edutools/static/js/interactive_code/turtleCode.js +56 -0
- munchboka_edutools/static/js/interactive_code/workerManager.js +204 -0
- munchboka_edutools/static/js/jeopardy.js +457 -0
- munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
- munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
- munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
- munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
- munchboka_edutools/static/js/quiz.js +422 -0
- munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
- munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
- munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
- munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
- munchboka_edutools/static/js/utils.js +3 -0
- munchboka_edutools-0.1.0.dist-info/METADATA +107 -0
- munchboka_edutools-0.1.0.dist-info/RECORD +150 -0
- munchboka_edutools-0.1.0.dist-info/WHEEL +4 -0
- munchboka_edutools-0.1.0.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
|
+
}
|