|
1 | 1 | import { useEffect, useState } from 'react'; |
2 | 2 | import { useAppContext } from '../utils/app.context'; |
3 | | -import { XCloseButton } from '../utils/common'; |
4 | | -import { delay } from '../utils/misc'; |
5 | | -import StorageUtils from '../utils/storage'; |
| 3 | +import { OpenInNewTab, XCloseButton } from '../utils/common'; |
6 | 4 | import { CanvasType } from '../utils/types'; |
7 | | -import { PlayIcon } from '@heroicons/react/24/outline'; |
8 | | - |
9 | | -const PyodideWrapper = { |
10 | | - load: async function () { |
11 | | - // load pyodide from CDN |
12 | | - // @ts-expect-error experimental pyodide |
13 | | - if (window.addedScriptPyodide) return; |
14 | | - // @ts-expect-error experimental pyodide |
15 | | - window.addedScriptPyodide = true; |
16 | | - const scriptElem = document.createElement('script'); |
17 | | - scriptElem.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js'; |
18 | | - document.body.appendChild(scriptElem); |
19 | | - }, |
20 | | - |
21 | | - run: async function (code: string) { |
22 | | - PyodideWrapper.load(); |
23 | | - |
24 | | - // wait for pyodide to be loaded |
25 | | - // @ts-expect-error experimental pyodide |
26 | | - while (!window.loadPyodide) { |
27 | | - await delay(100); |
28 | | - } |
29 | | - const stdOutAndErr: string[] = []; |
30 | | - // @ts-expect-error experimental pyodide |
31 | | - const pyodide = await window.loadPyodide({ |
32 | | - stdout: (data: string) => stdOutAndErr.push(data), |
33 | | - stderr: (data: string) => stdOutAndErr.push(data), |
34 | | - }); |
35 | | - try { |
36 | | - const result = await pyodide.runPythonAsync(code); |
37 | | - if (result) { |
38 | | - stdOutAndErr.push(result.toString()); |
| 5 | +import { PlayIcon, StopIcon } from '@heroicons/react/24/outline'; |
| 6 | + |
| 7 | +const canInterrupt = typeof SharedArrayBuffer === 'function'; |
| 8 | + |
| 9 | +// adapted from https://pyodide.org/en/stable/usage/webworker.html |
| 10 | +const WORKER_CODE = ` |
| 11 | +importScripts("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js"); |
| 12 | +
|
| 13 | +let stdOutAndErr = []; |
| 14 | +
|
| 15 | +let pyodideReadyPromise = loadPyodide({ |
| 16 | + stdout: (data) => stdOutAndErr.push(data), |
| 17 | + stderr: (data) => stdOutAndErr.push(data), |
| 18 | +}); |
| 19 | +
|
| 20 | +let alreadySetBuff = false; |
| 21 | +
|
| 22 | +self.onmessage = async (event) => { |
| 23 | + stdOutAndErr = []; |
| 24 | +
|
| 25 | + // make sure loading is done |
| 26 | + const pyodide = await pyodideReadyPromise; |
| 27 | + const { id, python, context, interruptBuffer } = event.data; |
| 28 | +
|
| 29 | + if (interruptBuffer && !alreadySetBuff) { |
| 30 | + pyodide.setInterruptBuffer(interruptBuffer); |
| 31 | + alreadySetBuff = true; |
| 32 | + } |
| 33 | +
|
| 34 | + // Now load any packages we need, run the code, and send the result back. |
| 35 | + await pyodide.loadPackagesFromImports(python); |
| 36 | +
|
| 37 | + // make a Python dictionary with the data from content |
| 38 | + const dict = pyodide.globals.get("dict"); |
| 39 | + const globals = dict(Object.entries(context)); |
| 40 | + try { |
| 41 | + self.postMessage({ id, running: true }); |
| 42 | + // Execute the python code in this context |
| 43 | + const result = pyodide.runPython(python, { globals }); |
| 44 | + self.postMessage({ result, id, stdOutAndErr }); |
| 45 | + } catch (error) { |
| 46 | + self.postMessage({ error: error.message, id }); |
| 47 | + } |
| 48 | + interruptBuffer[0] = 0; |
| 49 | +}; |
| 50 | +`; |
| 51 | + |
| 52 | +let worker: Worker; |
| 53 | +const interruptBuffer = canInterrupt |
| 54 | + ? new Uint8Array(new SharedArrayBuffer(1)) |
| 55 | + : null; |
| 56 | + |
| 57 | +const runCodeInWorker = ( |
| 58 | + pyCode: string, |
| 59 | + callbackRunning: () => void |
| 60 | +): { |
| 61 | + donePromise: Promise<string>; |
| 62 | + interrupt: () => void; |
| 63 | +} => { |
| 64 | + if (!worker) { |
| 65 | + worker = new Worker( |
| 66 | + URL.createObjectURL(new Blob([WORKER_CODE], { type: 'text/javascript' })) |
| 67 | + ); |
| 68 | + } |
| 69 | + const id = Math.random() * 1e8; |
| 70 | + const context = {}; |
| 71 | + if (interruptBuffer) { |
| 72 | + interruptBuffer[0] = 0; |
| 73 | + } |
| 74 | + |
| 75 | + const donePromise = new Promise<string>((resolve) => { |
| 76 | + worker.onmessage = (event) => { |
| 77 | + const { error, stdOutAndErr, running } = event.data; |
| 78 | + if (id !== event.data.id) return; |
| 79 | + if (running) { |
| 80 | + callbackRunning(); |
| 81 | + return; |
| 82 | + } else if (error) { |
| 83 | + resolve(error.toString()); |
| 84 | + } else { |
| 85 | + resolve(stdOutAndErr.join('\n')); |
39 | 86 | } |
40 | | - } catch (e) { |
41 | | - console.error(e); |
42 | | - stdOutAndErr.push((e as Error).toString()); |
| 87 | + }; |
| 88 | + worker.postMessage({ id, python: pyCode, context, interruptBuffer }); |
| 89 | + }); |
| 90 | + |
| 91 | + const interrupt = () => { |
| 92 | + console.log('Interrupting...'); |
| 93 | + console.trace(); |
| 94 | + if (interruptBuffer) { |
| 95 | + interruptBuffer[0] = 2; |
43 | 96 | } |
44 | | - return stdOutAndErr.join('\n'); |
45 | | - }, |
46 | | -}; |
| 97 | + }; |
47 | 98 |
|
48 | | -if (StorageUtils.getConfig().pyIntepreterEnabled) { |
49 | | - PyodideWrapper.load(); |
50 | | -} |
| 99 | + return { donePromise, interrupt }; |
| 100 | +}; |
51 | 101 |
|
52 | 102 | export default function CanvasPyInterpreter() { |
53 | 103 | const { canvasData, setCanvasData } = useAppContext(); |
54 | 104 |
|
55 | 105 | const [code, setCode] = useState(canvasData?.content ?? ''); // copy to avoid direct mutation |
56 | 106 | const [running, setRunning] = useState(false); |
57 | 107 | const [output, setOutput] = useState(''); |
| 108 | + const [interruptFn, setInterruptFn] = useState<() => void>(); |
| 109 | + const [showStopBtn, setShowStopBtn] = useState(false); |
58 | 110 |
|
59 | 111 | const runCode = async (pycode: string) => { |
| 112 | + interruptFn?.(); |
60 | 113 | setRunning(true); |
61 | | - setOutput('Running...'); |
62 | | - const out = await PyodideWrapper.run(pycode); |
| 114 | + setOutput('Loading Pyodide...'); |
| 115 | + const { donePromise, interrupt } = runCodeInWorker(pycode, () => { |
| 116 | + setOutput('Running...'); |
| 117 | + setShowStopBtn(canInterrupt); |
| 118 | + }); |
| 119 | + setInterruptFn(() => interrupt); |
| 120 | + const out = await donePromise; |
63 | 121 | setOutput(out); |
64 | 122 | setRunning(false); |
| 123 | + setShowStopBtn(false); |
65 | 124 | }; |
66 | 125 |
|
67 | 126 | // run code on mount |
@@ -98,9 +157,21 @@ export default function CanvasPyInterpreter() { |
98 | 157 | onClick={() => runCode(code)} |
99 | 158 | disabled={running} |
100 | 159 | > |
101 | | - <PlayIcon className="h-6 w-6" />{' '} |
102 | | - {running ? 'Running...' : 'Run'} |
| 160 | + <PlayIcon className="h-6 w-6" /> Run |
103 | 161 | </button> |
| 162 | + {showStopBtn && ( |
| 163 | + <button |
| 164 | + className="btn btn-sm bg-base-100 ml-2" |
| 165 | + onClick={() => interruptFn?.()} |
| 166 | + > |
| 167 | + <StopIcon className="h-6 w-6" /> Stop |
| 168 | + </button> |
| 169 | + )} |
| 170 | + <span className="grow text-right text-xs"> |
| 171 | + <OpenInNewTab href="https://github.com/ggerganov/llama.cpp/issues/11762"> |
| 172 | + Report a bug |
| 173 | + </OpenInNewTab> |
| 174 | + </span> |
104 | 175 | </div> |
105 | 176 | <textarea |
106 | 177 | className="textarea textarea-bordered h-full dark-color" |
|
0 commit comments