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,252 @@
|
|
|
1
|
+
class InteractiveCodeSetup {
|
|
2
|
+
constructor(containerId, initialCode, preloadPackages = null) {
|
|
3
|
+
this.containerId = containerId;
|
|
4
|
+
this.initialCode = initialCode;
|
|
5
|
+
this.preloadPackages = preloadPackages;
|
|
6
|
+
this.uniqueId = this.generateUUID();
|
|
7
|
+
|
|
8
|
+
// HTML element IDs
|
|
9
|
+
this.editorId = `code-editor-${this.uniqueId}`;
|
|
10
|
+
this.runButtonId = `run-button-${this.uniqueId}`;
|
|
11
|
+
this.resetButtonId = `reset-button-${this.uniqueId}`;
|
|
12
|
+
this.cancelButtonId = `cancel-button-${this.uniqueId}`;
|
|
13
|
+
this.outputId = `output-${this.uniqueId}`;
|
|
14
|
+
this.errorBoxId = `error-box-${this.uniqueId}`;
|
|
15
|
+
|
|
16
|
+
this.editorInstance = null;
|
|
17
|
+
this.runnerInstance = null;
|
|
18
|
+
|
|
19
|
+
this.createEditorHTML();
|
|
20
|
+
this.setupInteractiveEditor();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
generateUUID() {
|
|
24
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
25
|
+
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
26
|
+
return v.toString(16);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
createEditorHTML() {
|
|
31
|
+
const container = document.getElementById(this.containerId);
|
|
32
|
+
if (!container) return;
|
|
33
|
+
|
|
34
|
+
const runIcon = `
|
|
35
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
36
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
|
|
37
|
+
</svg>`;
|
|
38
|
+
const resetIcon = `
|
|
39
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
|
40
|
+
<path fill-rule="evenodd" d="M9.53 2.47a.75.75 0 0 1 0 1.06L4.81 8.25H15a6.75 6.75 0 0 1 0 13.5h-3a.75.75 0 0 1 0-1.5h3a5.25 5.25 0 1 0 0-10.5H4.81l4.72 4.72a.75.75 0 1 1-1.06 1.06l-6-6a.75.75 0 0 1 0-1.06l6-6a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
|
|
41
|
+
</svg>`;
|
|
42
|
+
const cancelIcon = `
|
|
43
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
44
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 7.5A2.25 2.25 0 0 1 7.5 5.25h9a2.25 2.25 0 0 1 2.25 2.25v9a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-9Z" />
|
|
45
|
+
</svg>`;
|
|
46
|
+
|
|
47
|
+
const html = `
|
|
48
|
+
<div>
|
|
49
|
+
<textarea id="${this.editorId}" name="code-${this.uniqueId}">${this.initialCode}</textarea>
|
|
50
|
+
|
|
51
|
+
<button id="${this.runButtonId}" class="button button-run">Kjør kode ${runIcon}</button>
|
|
52
|
+
<button id="${this.resetButtonId}" class="button button-reset">Reset kode ${resetIcon}</button>
|
|
53
|
+
<button id="${this.cancelButtonId}" class="button button-cancel">Avbryt kjøring ${cancelIcon}</button>
|
|
54
|
+
</div>
|
|
55
|
+
<div id="${this.errorBoxId}"></div>
|
|
56
|
+
<pre id="${this.outputId}" class="pythonoutput"></pre>
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
container.innerHTML = html;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setupInteractiveEditor() {
|
|
63
|
+
this.editorInstance = new CodeEditor(this.editorId);
|
|
64
|
+
this.runnerInstance = new PythonRunner(this.outputId, this.errorBoxId, this.preloadPackages);
|
|
65
|
+
|
|
66
|
+
// Wait for editor to be ready before attaching listeners
|
|
67
|
+
this.editorInstance.editorReady.then(() => {
|
|
68
|
+
const runBtn = document.getElementById(this.runButtonId);
|
|
69
|
+
const resetBtn = document.getElementById(this.resetButtonId);
|
|
70
|
+
const cancelBtn = document.getElementById(this.cancelButtonId);
|
|
71
|
+
|
|
72
|
+
if (runBtn) runBtn.addEventListener("click", () => this.runCode());
|
|
73
|
+
if (resetBtn) resetBtn.addEventListener("click", () => this.resetCode());
|
|
74
|
+
if (cancelBtn) cancelBtn.addEventListener("click", () => this.cancelCodeExecution());
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
runCode() {
|
|
79
|
+
this.clearOutput();
|
|
80
|
+
this.runnerInstance.run(this.editorInstance);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
resetCode() {
|
|
84
|
+
this.clearOutput();
|
|
85
|
+
this.editorInstance.resetEditor(this.initialCode);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cancelCodeExecution() {
|
|
89
|
+
if (this.runnerInstance.workerManager) {
|
|
90
|
+
this.runnerInstance.workerManager.restartWorker();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clearOutput() {
|
|
95
|
+
const out = document.getElementById(this.outputId);
|
|
96
|
+
const err = document.getElementById(this.errorBoxId);
|
|
97
|
+
if (out) out.textContent = "";
|
|
98
|
+
if (err) err.textContent = "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function makeInteractiveCode(containerId, initialCode, preloadPackages = null) {
|
|
103
|
+
return new InteractiveCodeSetup(containerId, initialCode, preloadPackages);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class PredictionInteractiveCodeSetup extends InteractiveCodeSetup {
|
|
107
|
+
constructor(containerId, initialCode) {
|
|
108
|
+
super(containerId, initialCode);
|
|
109
|
+
this.predictionInputId = `prediction-input-${this.uniqueId}`;
|
|
110
|
+
this.lockPredictionButtonId = `lock-prediction-button-${this.uniqueId}`;
|
|
111
|
+
this.predictionDisplayId = `prediction-display-${this.uniqueId}`;
|
|
112
|
+
this.predictionOutputId = `prediction-output-${this.uniqueId}`;
|
|
113
|
+
this.predictionOutputContainerId = `prediction-output-container-${this.uniqueId}`;
|
|
114
|
+
this.predictionContainerId = `prediction-container-${this.uniqueId}`;
|
|
115
|
+
|
|
116
|
+
this.predictionDisplayed = false;
|
|
117
|
+
|
|
118
|
+
this.addPredictionHTML();
|
|
119
|
+
this.setupPredictionFeature();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
addPredictionHTML() {
|
|
123
|
+
const container = document.getElementById(this.containerId);
|
|
124
|
+
if (!container) return;
|
|
125
|
+
|
|
126
|
+
const predictionHtml = `
|
|
127
|
+
<div id="${this.predictionContainerId}" class="prediction-container">
|
|
128
|
+
<textarea id="${this.predictionInputId}" rows="3" placeholder="Skriv inn svaret ditt her! \n \nTrykk på Enter (⏎) for en ny linje."></textarea>
|
|
129
|
+
<button id="${this.lockPredictionButtonId}" class="button button-run">Sjekk svaret!</button>
|
|
130
|
+
</div>
|
|
131
|
+
`;
|
|
132
|
+
container.insertAdjacentHTML('beforeend', predictionHtml);
|
|
133
|
+
|
|
134
|
+
const predictionOutputContainer = document.createElement('div');
|
|
135
|
+
predictionOutputContainer.id = this.predictionOutputContainerId;
|
|
136
|
+
predictionOutputContainer.className = 'prediction-output-container';
|
|
137
|
+
predictionOutputContainer.style.display = 'none';
|
|
138
|
+
|
|
139
|
+
const predictionDisplay = document.createElement('div');
|
|
140
|
+
predictionDisplay.className = 'prediction-display';
|
|
141
|
+
predictionDisplay.innerHTML = `
|
|
142
|
+
<h3>Ditt svar:</h3>
|
|
143
|
+
<pre id="${this.predictionDisplayId}"></pre>
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
const outputDisplay = document.createElement('div');
|
|
147
|
+
outputDisplay.className = 'output-display';
|
|
148
|
+
outputDisplay.innerHTML = `
|
|
149
|
+
<h3>Faktisk utskrift:</h3>
|
|
150
|
+
<pre id="${this.predictionOutputId}" class="pythonoutput"></pre>
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
predictionOutputContainer.appendChild(predictionDisplay);
|
|
154
|
+
predictionOutputContainer.appendChild(outputDisplay);
|
|
155
|
+
|
|
156
|
+
const errorBoxElement = document.getElementById(this.errorBoxId);
|
|
157
|
+
if (errorBoxElement) {
|
|
158
|
+
errorBoxElement.insertAdjacentElement('afterend', predictionOutputContainer);
|
|
159
|
+
} else {
|
|
160
|
+
container.appendChild(predictionOutputContainer);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.originalOutputElement = document.getElementById(this.outputId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setupPredictionFeature() {
|
|
167
|
+
// Wait for editor to be ready before setting read-only and attaching listeners
|
|
168
|
+
this.editorInstance.editorReady.then(() => {
|
|
169
|
+
this.editorInstance.editor.setOption('readOnly', true);
|
|
170
|
+
const rb = document.getElementById(this.runButtonId);
|
|
171
|
+
const rsb = document.getElementById(this.resetButtonId);
|
|
172
|
+
const cb = document.getElementById(this.cancelButtonId);
|
|
173
|
+
if (rb) rb.style.display = 'none';
|
|
174
|
+
if (rsb) rsb.style.display = 'none';
|
|
175
|
+
if (cb) cb.style.display = 'none';
|
|
176
|
+
|
|
177
|
+
const lockBtn = document.getElementById(this.lockPredictionButtonId);
|
|
178
|
+
if (lockBtn) lockBtn.addEventListener("click", () => this.lockPrediction());
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
lockPrediction() {
|
|
183
|
+
const prediction = (document.getElementById(this.predictionInputId) || { value: '' }).value;
|
|
184
|
+
this.prediction = prediction;
|
|
185
|
+
|
|
186
|
+
const predictionContainer = document.getElementById(this.predictionContainerId);
|
|
187
|
+
if (predictionContainer) predictionContainer.style.display = 'none';
|
|
188
|
+
|
|
189
|
+
// Wait for editor to be ready before modifying
|
|
190
|
+
this.editorInstance.editorReady.then(() => {
|
|
191
|
+
this.editorInstance.editor.setOption('readOnly', false);
|
|
192
|
+
|
|
193
|
+
const rb = document.getElementById(this.runButtonId);
|
|
194
|
+
const rsb = document.getElementById(this.resetButtonId);
|
|
195
|
+
const cb = document.getElementById(this.cancelButtonId);
|
|
196
|
+
if (rb) rb.style.display = 'inline-block';
|
|
197
|
+
if (rsb) rsb.style.display = 'inline-block';
|
|
198
|
+
if (cb) cb.style.display = 'inline-block';
|
|
199
|
+
|
|
200
|
+
this.runCode();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
runCode() {
|
|
205
|
+
if (this.predictionDisplayed) {
|
|
206
|
+
this.initialCode = this.editorInstance.getValue();
|
|
207
|
+
this.replaceWithInteractiveCodeSetup();
|
|
208
|
+
} else {
|
|
209
|
+
this.clearOutput();
|
|
210
|
+
this.runnerInstance.run(this.editorInstance, this.predictionOutputId);
|
|
211
|
+
this.displayPredictionAndOutput();
|
|
212
|
+
this.predictionDisplayed = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
resetCode() {
|
|
217
|
+
if (this.predictionDisplayed) {
|
|
218
|
+
this.replaceWithInteractiveCodeSetup();
|
|
219
|
+
} else {
|
|
220
|
+
this.clearOutput();
|
|
221
|
+
this.editorInstance.resetEditor(this.initialCode);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
cancelCodeExecution() {
|
|
226
|
+
if (this.predictionDisplayed) {
|
|
227
|
+
this.replaceWithInteractiveCodeSetup();
|
|
228
|
+
} else if (this.runnerInstance.workerManager) {
|
|
229
|
+
this.runnerInstance.workerManager.restartWorker();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
replaceWithInteractiveCodeSetup() {
|
|
234
|
+
const container = document.getElementById(this.containerId);
|
|
235
|
+
if (container) {
|
|
236
|
+
container.innerHTML = '';
|
|
237
|
+
new InteractiveCodeSetup(this.containerId, this.initialCode);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
displayPredictionAndOutput() {
|
|
242
|
+
const pred = document.getElementById(this.predictionDisplayId);
|
|
243
|
+
if (pred) pred.textContent = this.prediction;
|
|
244
|
+
if (this.originalOutputElement) this.originalOutputElement.style.display = 'none';
|
|
245
|
+
const poc = document.getElementById(this.predictionOutputContainerId);
|
|
246
|
+
if (poc) poc.style.display = 'flex';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function makePredictionInteractiveCode(containerId, initialCode) {
|
|
251
|
+
return new PredictionInteractiveCodeSetup(containerId, initialCode);
|
|
252
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Ported pythonRunner.js (simplified) - full version lives in original book assets
|
|
2
|
+
// This is a near-direct copy from matematikk_r1 interactiveCode/pythonRunner.js
|
|
3
|
+
|
|
4
|
+
class PythonRunner {
|
|
5
|
+
constructor(outputId, errorBoxId, preloadPackages = ['casify']) {
|
|
6
|
+
this.outputId = outputId;
|
|
7
|
+
this.errorBoxId = errorBoxId;
|
|
8
|
+
this.workerManager = WorkerManager.getInstance(preloadPackages);
|
|
9
|
+
this.preloadPackages = preloadPackages;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async run(editor, outputId = null) {
|
|
13
|
+
try {
|
|
14
|
+
await this.workerManager.workerReadyPromise;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error("Worker failed to initialize:", error);
|
|
17
|
+
this.handleErrorMessage("Failed to initialize Python environment.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.editorInstance = editor;
|
|
22
|
+
this.editorInstance.clearLineHighlights();
|
|
23
|
+
let code = editor.getValue();
|
|
24
|
+
this.currentCode = code;
|
|
25
|
+
if (outputId) this.outputId = outputId;
|
|
26
|
+
|
|
27
|
+
const inputStatements = this.findInputStatements(this.currentCode);
|
|
28
|
+
if (inputStatements.length > 0) {
|
|
29
|
+
const userValues = await this.getUserInputs(inputStatements);
|
|
30
|
+
this.currentCode = this.replaceInputStatements(this.currentCode, userValues);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const packages = this.extractPackageNames(this.currentCode);
|
|
34
|
+
if (!packages.includes('matplotlib')) packages.push('matplotlib');
|
|
35
|
+
if (packages.length > 0) {
|
|
36
|
+
try { await this.workerManager.loadPackages(packages); } catch (error) {
|
|
37
|
+
console.error("Failed to load packages:", error);
|
|
38
|
+
this.handleErrorMessage(error.message);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const callback = (data) => {
|
|
44
|
+
if (data.type === 'stdout') {
|
|
45
|
+
this.handleWorkerMessage(data);
|
|
46
|
+
} else if (data.type === 'stderr') {
|
|
47
|
+
this.handleErrorMessage(data.msg);
|
|
48
|
+
} else if (data.type === 'plot') {
|
|
49
|
+
const outputElement = document.getElementById(this.outputId);
|
|
50
|
+
if (!outputElement) return;
|
|
51
|
+
const img = document.createElement('img');
|
|
52
|
+
img.src = 'data:image/png;base64,' + data.data;
|
|
53
|
+
img.style.width = '100%';
|
|
54
|
+
img.style.height = 'auto';
|
|
55
|
+
img.style.maxHeight = '500px';
|
|
56
|
+
outputElement.appendChild(img);
|
|
57
|
+
this.scrollToBottom(outputElement);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
this.workerManager.runCode(this.currentCode, callback);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
handleWorkerMessage(data) {
|
|
64
|
+
const { type, msg } = data;
|
|
65
|
+
const outputElement = document.getElementById(this.outputId);
|
|
66
|
+
if (!outputElement) return;
|
|
67
|
+
if (type === 'stdout') {
|
|
68
|
+
let formattedMsg = msg
|
|
69
|
+
.replace(/And\(([^)]+)\)/g, (match, p1) => {
|
|
70
|
+
const conditions = p1.split(',').map(cond => cond.trim());
|
|
71
|
+
return conditions.map(cond => `(${cond})`).join(' ∧ ');
|
|
72
|
+
})
|
|
73
|
+
.replace(/oo/g, '∞')
|
|
74
|
+
.replace(/\|/g, '∨');
|
|
75
|
+
outputElement.innerHTML += this.formatErrorMessage(formattedMsg);
|
|
76
|
+
this.highlightLine(this.editorInstance, data.msg);
|
|
77
|
+
this.scrollToBottom(outputElement);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
scrollToBottom(element) { element.scrollTop = element.scrollHeight; }
|
|
82
|
+
|
|
83
|
+
handleErrorMessage(msg) {
|
|
84
|
+
const errorElement = document.getElementById(this.errorBoxId);
|
|
85
|
+
if (errorElement) errorElement.innerHTML = this.formatErrorMessage(msg);
|
|
86
|
+
this.highlightLine(this.editorInstance, msg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
formatErrorMessage(errorMsg) {
|
|
90
|
+
let formattedMessage = errorMsg;
|
|
91
|
+
const fileLinePattern = /File "<exec>", line (\d+)/g;
|
|
92
|
+
formattedMessage = formattedMessage.replace(fileLinePattern, (match, p1) => match.replace(`line ${p1}`, `<span class="error-line">line ${p1}</span>`));
|
|
93
|
+
const errorTypeMatch = errorMsg.match(/(\w+Error):/);
|
|
94
|
+
if (errorTypeMatch) formattedMessage = formattedMessage.replace(errorTypeMatch[1], `<span class="error-type">${errorTypeMatch[1]}</span>`);
|
|
95
|
+
return formattedMessage;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
extractPackageNames(code) {
|
|
99
|
+
const importRegex = /^\s*import\s+([^;\s]+)\s*/gm;
|
|
100
|
+
const fromImportRegex = /^\s*from\s+([^;\s]+)\s+import/gm;
|
|
101
|
+
const packages = new Set();
|
|
102
|
+
const standardLibs = new Set(['abc','argparse','array','ast','asyncio','base64','binascii','bisect','bz2','calendar','collections','contextlib','copy','csv','dataclasses','datetime','functools','hashlib','heapq','html','http','io','itertools','json','logging','math','operator','os','pathlib','pickle','platform','random','re','statistics','string','sys','textwrap','time','typing','unittest','urllib','uuid']);
|
|
103
|
+
let match;
|
|
104
|
+
while ((match = importRegex.exec(code)) !== null) { const pn = match[1].split('.')[0]; if (!standardLibs.has(pn)) packages.add(pn); }
|
|
105
|
+
while ((match = fromImportRegex.exec(code)) !== null) { const pn = match[1].split('.')[0]; if (!standardLibs.has(pn)) packages.add(pn); }
|
|
106
|
+
return Array.from(packages);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
findInputStatements(code) {
|
|
110
|
+
// Python identifiers with Unicode: first char letter/_ then letters/digits/_
|
|
111
|
+
const inputRegex = /([\p{L}_][\p{L}\p{N}_]*)\s*=\s*(int|float|eval)?\(?input\(["'](.*?)["']\)\)?/gu;
|
|
112
|
+
let match; const inputs = [];
|
|
113
|
+
while ((match = inputRegex.exec(code)) !== null) { inputs.push({ variable: match[1], promptText: match[3] }); }
|
|
114
|
+
return inputs;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getUserInputs(inputs) {
|
|
118
|
+
const userValues = {}; for (const input of inputs) { userValues[input.variable] = await this.promptUser(input.promptText.replace(/["']+/g, '')); } return userValues;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
promptUser(promptText) { return new Promise((resolve) => { resolve(prompt(promptText)); }); }
|
|
122
|
+
|
|
123
|
+
replaceInputStatements(code, userValues) {
|
|
124
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
125
|
+
let codeLines = code.split('\n');
|
|
126
|
+
codeLines = codeLines.map(line => {
|
|
127
|
+
for (let variable in userValues) {
|
|
128
|
+
const vEsc = escapeRe(variable);
|
|
129
|
+
// Match beginning or non-identifier char before var to avoid partial matches
|
|
130
|
+
const inputRegex = new RegExp(`(^|[^\\p{L}\\p{N}_])(${vEsc})\\s*=\\s*(?:float|int|eval)?\\(?input\\(.*?\\)\\)?`, 'gu');
|
|
131
|
+
line = line.replace(inputRegex, (m, prefix, v) => {
|
|
132
|
+
let userValue = userValues[variable];
|
|
133
|
+
if (isNaN(userValue) && typeof userValue === 'string') userValue = `"${userValue}"`;
|
|
134
|
+
return `${prefix}${v} = ${userValue}`;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return line;
|
|
138
|
+
});
|
|
139
|
+
return codeLines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
highlightLine(editor, msg) {
|
|
143
|
+
const linePattern = /File "<exec>", line (\d+)/; const match = linePattern.exec(msg); if (match) { const lineNumber = parseInt(match[1]) - 1; editor.highlightLine(lineNumber); }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Ported turtleCode.js minimal wrapper that uses CodeEditor + Skulpt
|
|
2
|
+
|
|
3
|
+
function generateUUID() {
|
|
4
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
5
|
+
const r = (Math.random() * 16) | 0;
|
|
6
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
7
|
+
return v.toString(16);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class TurtleCode {
|
|
12
|
+
constructor(containerId, initialCode = "", cmOptions = {}) {
|
|
13
|
+
this.containerId = containerId;
|
|
14
|
+
this.container = document.getElementById(containerId);
|
|
15
|
+
if (!this.container) throw new Error(`Container ${containerId} not found`);
|
|
16
|
+
this.initialCode = initialCode;
|
|
17
|
+
this.cmOptions = cmOptions;
|
|
18
|
+
this.uniqueSuffix = generateUUID();
|
|
19
|
+
this.createUI();
|
|
20
|
+
this.editor = new CodeEditor(this.textAreaEl.id);
|
|
21
|
+
setTimeout(() => { this.editor.setValue(this.initialCode); }, 200);
|
|
22
|
+
this.runButtonEl.addEventListener("click", () => this.runCode());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
createUI() {
|
|
26
|
+
this.container.innerHTML = "";
|
|
27
|
+
const wrap = document.createElement('div');
|
|
28
|
+
wrap.style.display = 'flex'; wrap.style.flexWrap = 'wrap'; wrap.style.width = '100%';
|
|
29
|
+
const left = document.createElement('div'); left.style.flex = '1'; left.style.minWidth = '220px'; left.style.display='flex'; left.style.flexDirection='column';
|
|
30
|
+
const right = document.createElement('div'); right.style.flex = '1'; right.style.minWidth = '320px'; right.style.display='flex'; right.style.flexDirection='column'; right.style.alignItems='center'; right.style.justifyContent='center';
|
|
31
|
+
this.textAreaEl = document.createElement('textarea'); this.textAreaEl.id = `skulpt-editor-${this.uniqueSuffix}`; this.textAreaEl.style.display='none'; left.appendChild(this.textAreaEl);
|
|
32
|
+
this.runButtonEl = document.createElement('button'); this.runButtonEl.className='button button-run'; this.runButtonEl.textContent='Kjør kode'; this.runButtonEl.style.margin='1em 0'; left.appendChild(this.runButtonEl);
|
|
33
|
+
this.outputEl = document.createElement('pre'); this.outputEl.className='pythonoutput'; this.outputEl.style.minHeight='2em'; left.appendChild(this.outputEl);
|
|
34
|
+
this.canvasEl = document.createElement('div'); this.canvasEl.id = `skulpt-canvas-${this.uniqueSuffix}`; this.canvasEl.style.border='1px solid #ccc'; this.canvasEl.style.width='95%'; this.canvasEl.style.height='400px'; right.appendChild(this.canvasEl);
|
|
35
|
+
wrap.appendChild(left); wrap.appendChild(right); this.container.appendChild(wrap);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
runCode() {
|
|
39
|
+
let userCode = this.editor.getValue();
|
|
40
|
+
this.outputEl.innerHTML = '';
|
|
41
|
+
const mode = document.documentElement.getAttribute('data-mode');
|
|
42
|
+
let forcedColor = 'black';
|
|
43
|
+
if (mode === 'dark') forcedColor = 'white';
|
|
44
|
+
else if (mode === 'auto') forcedColor = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'white' : 'black';
|
|
45
|
+
const snippet = `\ntry:\n import turtle\n turtle.color("${forcedColor}")\n screen = turtle.Screen()\nexcept: pass\n`;
|
|
46
|
+
userCode = snippet + userCode;
|
|
47
|
+
const outf = (text) => { this.outputEl.innerHTML += text; };
|
|
48
|
+
const builtinRead = (filename) => { if (!Sk.builtinFiles || !Sk.builtinFiles['files'][filename]) { throw new Error('File not found: ' + filename); } return Sk.builtinFiles['files'][filename]; };
|
|
49
|
+
Sk.pre = this.outputEl.id;
|
|
50
|
+
Sk.configure({ output: outf, read: builtinRead, python3: true });
|
|
51
|
+
(Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = this.canvasEl.id;
|
|
52
|
+
Sk.misceval.asyncToPromise(() => Sk.importMainWithBody('<stdin>', false, userCode, true)).catch(err => { this.outputEl.innerHTML = err.toString(); });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeTurtleCode(containerId, initialCode = '', cmOptions = {}) { return new TurtleCode(containerId, initialCode, cmOptions); }
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Ported workerManager.js from matematikk_r1 interactiveCode
|
|
2
|
+
|
|
3
|
+
class WorkerManager {
|
|
4
|
+
static instance = null;
|
|
5
|
+
|
|
6
|
+
static getInstance(preloadPackages = null) {
|
|
7
|
+
if (!WorkerManager.instance) {
|
|
8
|
+
const defaultPreloadPackages = ['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip'];
|
|
9
|
+
const combinedPreloadPackages = Array.from(new Set(preloadPackages ? [...defaultPreloadPackages, ...preloadPackages] : defaultPreloadPackages));
|
|
10
|
+
WorkerManager.instance = new WorkerManager(combinedPreloadPackages);
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
WorkerManager.instance.warmUpPyodide().catch(err => console.warn('Pyodide warm-up failed:', err));
|
|
13
|
+
}, 1000);
|
|
14
|
+
} else if (preloadPackages) {
|
|
15
|
+
const packagesToLoad = preloadPackages.filter(pkg => !WorkerManager.instance.loadedPackages.has(pkg));
|
|
16
|
+
const combinedPreloadPackages = Array.from(new Set(['matplotlib', 'numpy', 'scipy', 'sympy', 'micropip', ...packagesToLoad]));
|
|
17
|
+
if (combinedPreloadPackages.length > 0) WorkerManager.instance.loadPackages(combinedPreloadPackages);
|
|
18
|
+
}
|
|
19
|
+
return WorkerManager.instance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor(preloadPackages = []) {
|
|
23
|
+
if (WorkerManager.instance) return WorkerManager.instance;
|
|
24
|
+
this.worker = null;
|
|
25
|
+
this.callbacks = {};
|
|
26
|
+
this.preloadPackages = preloadPackages;
|
|
27
|
+
this.loadedPackages = new Set();
|
|
28
|
+
this.packageLoadPromises = {};
|
|
29
|
+
this.workerReadyPromise = new Promise((resolve, reject) => { this.workerReadyResolve = resolve; this.workerReadyReject = reject; });
|
|
30
|
+
this.initWorker();
|
|
31
|
+
WorkerManager.instance = this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
initWorker() {
|
|
35
|
+
// Inline Python template kept inside JS string carefully escaped.
|
|
36
|
+
const workerScript = `importScripts('https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js');
|
|
37
|
+
|
|
38
|
+
let pyodideReadyPromise = loadPyodide();
|
|
39
|
+
let initialGlobals = new Set();
|
|
40
|
+
|
|
41
|
+
async function resetPyodide(pyodide, initialGlobals) {
|
|
42
|
+
const currentGlobals = new Set(pyodide.globals.keys());
|
|
43
|
+
const globalsToClear = Array.from(currentGlobals).filter(x => !initialGlobals.has(x));
|
|
44
|
+
for (const key of globalsToClear) { pyodide.globals.delete(key); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function installPackages(pyodide, packages) {
|
|
48
|
+
if (packages.length > 0) {
|
|
49
|
+
await pyodide.loadPackage('micropip');
|
|
50
|
+
const micropip = pyodide.pyimport('micropip');
|
|
51
|
+
await micropip.install(packages);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onmessage = async (event) => {
|
|
56
|
+
const messageId = event.data.messageId;
|
|
57
|
+
if (event.data.type === 'init') {
|
|
58
|
+
const { preloadPackages } = event.data;
|
|
59
|
+
const pyodide = await pyodideReadyPromise;
|
|
60
|
+
initialGlobals = new Set(pyodide.globals.keys());
|
|
61
|
+
const pyodidePackages = preloadPackages.filter(pkg => ['matplotlib','numpy','scipy','sympy','micropip'].includes(pkg));
|
|
62
|
+
const pypiPackages = preloadPackages.filter(pkg => !['matplotlib','numpy','scipy','sympy','micropip'].includes(pkg));
|
|
63
|
+
if (pyodidePackages.length > 0) await pyodide.loadPackage(pyodidePackages);
|
|
64
|
+
await installPackages(pyodide, pypiPackages);
|
|
65
|
+
postMessage(JSON.stringify({ type: 'initReady' }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (event.data.type === 'runCode') {
|
|
69
|
+
const { code } = event.data;
|
|
70
|
+
try {
|
|
71
|
+
const pyodide = await pyodideReadyPromise;
|
|
72
|
+
await resetPyodide(pyodide, initialGlobals);
|
|
73
|
+
const pyCode = \`import sys, json, io, base64
|
|
74
|
+
from js import postMessage
|
|
75
|
+
import matplotlib
|
|
76
|
+
matplotlib.use('Agg')
|
|
77
|
+
import matplotlib.pyplot as plt
|
|
78
|
+
|
|
79
|
+
MESSAGE_ID = "${messageId}"
|
|
80
|
+
|
|
81
|
+
class PyConsole:
|
|
82
|
+
def __init__(self, message_id):
|
|
83
|
+
self.message_id = message_id
|
|
84
|
+
self.buffer = ''
|
|
85
|
+
def write(self, msg):
|
|
86
|
+
self.buffer += msg
|
|
87
|
+
if '\\\\\\\\n' in msg:
|
|
88
|
+
self.flush()
|
|
89
|
+
def flush(self):
|
|
90
|
+
if self.buffer:
|
|
91
|
+
postMessage(json.dumps({'type':'stdout','msg':self.buffer,'messageId':self.message_id}))
|
|
92
|
+
self.buffer = ''
|
|
93
|
+
|
|
94
|
+
sys.stdout = PyConsole(MESSAGE_ID)
|
|
95
|
+
sys.stderr = PyConsole(MESSAGE_ID)
|
|
96
|
+
|
|
97
|
+
def show_override():
|
|
98
|
+
buf = io.BytesIO()
|
|
99
|
+
plt.savefig(buf, format='png')
|
|
100
|
+
fig = plt.gcf()
|
|
101
|
+
width_in = fig.get_figwidth(); height_in = fig.get_figheight(); dpi = fig.get_dpi()
|
|
102
|
+
width_px = int(width_in * dpi); height_px = int(height_in * dpi)
|
|
103
|
+
buf.seek(0)
|
|
104
|
+
image_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
|
105
|
+
postMessage(json.dumps({'type':'plot','data':image_base64,'messageId':MESSAGE_ID,'width':width_px,'height':height_px}))
|
|
106
|
+
plt.clf()
|
|
107
|
+
sys.stdout.write('\\\\\\\\n')
|
|
108
|
+
sys.stdout.write('\\\\\\\\n')
|
|
109
|
+
sys.stdout.flush()
|
|
110
|
+
|
|
111
|
+
plt.show = lambda: show_override()\`;
|
|
112
|
+
// Send back the Python code for debugging before execution
|
|
113
|
+
postMessage(JSON.stringify({ type: 'debugPyCode', messageId, pyCode }));
|
|
114
|
+
await pyodide.runPythonAsync(pyCode);
|
|
115
|
+
await pyodide.runPythonAsync(code);
|
|
116
|
+
// Ensure any buffered output gets emitted even if no trailing newline
|
|
117
|
+
await pyodide.runPythonAsync('import sys; sys.stdout.flush(); sys.stderr.flush()');
|
|
118
|
+
postMessage(JSON.stringify({ type: 'executionComplete', messageId }));
|
|
119
|
+
} catch (err) {
|
|
120
|
+
postMessage(JSON.stringify({ type: 'stderr', msg: String(err), messageId }));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (event.data.type === 'loadPackage') {
|
|
125
|
+
const { packages, packageRequestId } = event.data;
|
|
126
|
+
try {
|
|
127
|
+
const pyodide = await pyodideReadyPromise;
|
|
128
|
+
const pyodidePackages = packages.filter(pkg => ['matplotlib','numpy','scipy','sympy','micropip'].includes(pkg));
|
|
129
|
+
const pypiPackages = packages.filter(pkg => !['matplotlib','numpy','scipy','sympy','micropip'].includes(pkg));
|
|
130
|
+
if (pyodidePackages.length > 0) await pyodide.loadPackage(pyodidePackages);
|
|
131
|
+
await installPackages(pyodide, pypiPackages);
|
|
132
|
+
postMessage(JSON.stringify({ type: 'packagesLoaded', packageRequestId }));
|
|
133
|
+
} catch (err) {
|
|
134
|
+
postMessage(JSON.stringify({ type: 'stderr', msg: String(err), packageRequestId }));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
`;
|
|
139
|
+
const workerBlob = new Blob([workerScript], { type: 'application/javascript' });
|
|
140
|
+
this.worker = new Worker(URL.createObjectURL(workerBlob));
|
|
141
|
+
this.worker.onmessage = this.handleMessage.bind(this);
|
|
142
|
+
this.worker.onerror = this.handleError.bind(this);
|
|
143
|
+
this.worker.postMessage({ type: 'init', preloadPackages: this.preloadPackages });
|
|
144
|
+
this.pyodideWarmedUp = false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
warmUpPyodide() {
|
|
148
|
+
if (this.pyodideWarmedUp) return Promise.resolve();
|
|
149
|
+
return this.workerReadyPromise.then(() => new Promise((resolve) => {
|
|
150
|
+
const messageId = this.generateMessageId();
|
|
151
|
+
this.callbacks[messageId] = (data) => { if (data.type === 'executionComplete') { this.pyodideWarmedUp = true; resolve(); } };
|
|
152
|
+
this.worker.postMessage({ type: 'runCode', code: '# warmup', messageId });
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
generateMessageId() { return 'msg-' + Math.random().toString(36).substr(2, 9); }
|
|
157
|
+
|
|
158
|
+
loadPackages(packages) {
|
|
159
|
+
const packagesToLoad = packages.filter(pkg => !this.loadedPackages.has(pkg));
|
|
160
|
+
if (packagesToLoad.length === 0) return Promise.resolve();
|
|
161
|
+
const packageRequestId = 'pkg-' + Math.random().toString(36).substr(2, 9);
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
this.packageLoadPromises[packageRequestId] = { resolve, reject, packages: packagesToLoad };
|
|
164
|
+
this.worker.postMessage({ type: 'loadPackage', packages: packagesToLoad, packageRequestId });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
runCode(code, onMessageCallback) {
|
|
169
|
+
const messageId = this.generateMessageId();
|
|
170
|
+
this.callbacks[messageId] = onMessageCallback;
|
|
171
|
+
this.worker.postMessage({ type: 'runCode', code, messageId });
|
|
172
|
+
return messageId;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
handleMessage(event) {
|
|
176
|
+
let data; try { data = JSON.parse(event.data); } catch (e) { console.error('Worker message JSON parse failed:', e, event.data); return; }
|
|
177
|
+
const { messageId, packageRequestId } = data;
|
|
178
|
+
if (data.type === 'debugPyCode') {
|
|
179
|
+
// Surface embedded Python for inspection
|
|
180
|
+
console.log('Embedded Python received (messageId=' + data.messageId + '):\n' + data.pyCode);
|
|
181
|
+
}
|
|
182
|
+
if (messageId && this.callbacks[messageId]) {
|
|
183
|
+
this.callbacks[messageId](data);
|
|
184
|
+
if (data.type === 'executionComplete') delete this.callbacks[messageId];
|
|
185
|
+
} else if (packageRequestId && this.packageLoadPromises[packageRequestId]) {
|
|
186
|
+
const pkgPromise = this.packageLoadPromises[packageRequestId];
|
|
187
|
+
if (data.type === 'packagesLoaded') { for (const pkg of pkgPromise.packages) this.loadedPackages.add(pkg); pkgPromise.resolve(); }
|
|
188
|
+
else if (data.type === 'stderr') { pkgPromise.reject(new Error(data.msg)); }
|
|
189
|
+
delete this.packageLoadPromises[packageRequestId];
|
|
190
|
+
} else if (data.type === 'initReady') {
|
|
191
|
+
this.workerReadyResolve();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
handleError(error) { console.error('Worker error:', error); if (this.workerReadyReject) this.workerReadyReject(error); }
|
|
196
|
+
|
|
197
|
+
restartWorker() {
|
|
198
|
+
if (this.worker) { this.worker.terminate(); this.worker = null; }
|
|
199
|
+
this.loadedPackages = new Set(); this.pyodideWarmedUp = false;
|
|
200
|
+
this.workerReadyPromise = new Promise((resolve, reject) => { this.workerReadyResolve = resolve; this.workerReadyReject = reject; });
|
|
201
|
+
this.initWorker();
|
|
202
|
+
setTimeout(() => { this.warmUpPyodide().catch(err => console.warn('Warm-up failed after restart:', err)); }, 1000);
|
|
203
|
+
}
|
|
204
|
+
}
|