| | import { useEffect, useState } from 'react'; |
| | import { useAppContext } from '../utils/app.context'; |
| | import { OpenInNewTab, XCloseButton } from '../utils/common'; |
| | import { CanvasType } from '../utils/types'; |
| | import { PlayIcon, StopIcon } from '@heroicons/react/24/outline'; |
| | import StorageUtils from '../utils/storage'; |
| |
|
| | const canInterrupt = typeof SharedArrayBuffer === 'function'; |
| |
|
| | |
| | const WORKER_CODE = ` |
| | importScripts("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js"); |
| | |
| | let stdOutAndErr = []; |
| | |
| | let pyodideReadyPromise = loadPyodide({ |
| | stdout: (data) => stdOutAndErr.push(data), |
| | stderr: (data) => stdOutAndErr.push(data), |
| | }); |
| | |
| | let alreadySetBuff = false; |
| | |
| | self.onmessage = async (event) => { |
| | stdOutAndErr = []; |
| | |
| | // make sure loading is done |
| | const pyodide = await pyodideReadyPromise; |
| | const { id, python, context, interruptBuffer } = event.data; |
| | |
| | if (interruptBuffer && !alreadySetBuff) { |
| | pyodide.setInterruptBuffer(interruptBuffer); |
| | alreadySetBuff = true; |
| | } |
| | |
| | // Now load any packages we need, run the code, and send the result back. |
| | await pyodide.loadPackagesFromImports(python); |
| | |
| | // make a Python dictionary with the data from content |
| | const dict = pyodide.globals.get("dict"); |
| | const globals = dict(Object.entries(context)); |
| | try { |
| | self.postMessage({ id, running: true }); |
| | // Execute the python code in this context |
| | const result = pyodide.runPython(python, { globals }); |
| | self.postMessage({ result, id, stdOutAndErr }); |
| | } catch (error) { |
| | self.postMessage({ error: error.message, id }); |
| | } |
| | interruptBuffer[0] = 0; |
| | }; |
| | `; |
| |
|
| | let worker: Worker; |
| | const interruptBuffer = canInterrupt |
| | ? new Uint8Array(new SharedArrayBuffer(1)) |
| | : null; |
| |
|
| | const startWorker = () => { |
| | if (!worker) { |
| | worker = new Worker( |
| | URL.createObjectURL(new Blob([WORKER_CODE], { type: 'text/javascript' })) |
| | ); |
| | } |
| | }; |
| |
|
| | if (StorageUtils.getConfig().pyIntepreterEnabled) { |
| | startWorker(); |
| | } |
| |
|
| | const runCodeInWorker = ( |
| | pyCode: string, |
| | callbackRunning: () => void |
| | ): { |
| | donePromise: Promise<string>; |
| | interrupt: () => void; |
| | } => { |
| | startWorker(); |
| | const id = Math.random() * 1e8; |
| | const context = {}; |
| | if (interruptBuffer) { |
| | interruptBuffer[0] = 0; |
| | } |
| |
|
| | const donePromise = new Promise<string>((resolve) => { |
| | worker.onmessage = (event) => { |
| | const { error, stdOutAndErr, running } = event.data; |
| | if (id !== event.data.id) return; |
| | if (running) { |
| | callbackRunning(); |
| | return; |
| | } else if (error) { |
| | resolve(error.toString()); |
| | } else { |
| | resolve(stdOutAndErr.join('\n')); |
| | } |
| | }; |
| | worker.postMessage({ id, python: pyCode, context, interruptBuffer }); |
| | }); |
| |
|
| | const interrupt = () => { |
| | console.log('Interrupting...'); |
| | console.trace(); |
| | if (interruptBuffer) { |
| | interruptBuffer[0] = 2; |
| | } |
| | }; |
| |
|
| | return { donePromise, interrupt }; |
| | }; |
| |
|
| | export default function CanvasPyInterpreter() { |
| | const { canvasData, setCanvasData } = useAppContext(); |
| |
|
| | const [code, setCode] = useState(canvasData?.content ?? ''); |
| | const [running, setRunning] = useState(false); |
| | const [output, setOutput] = useState(''); |
| | const [interruptFn, setInterruptFn] = useState<() => void>(); |
| | const [showStopBtn, setShowStopBtn] = useState(false); |
| |
|
| | const runCode = async (pycode: string) => { |
| | interruptFn?.(); |
| | setRunning(true); |
| | setOutput('Loading Pyodide...'); |
| | const { donePromise, interrupt } = runCodeInWorker(pycode, () => { |
| | setOutput('Running...'); |
| | setShowStopBtn(canInterrupt); |
| | }); |
| | setInterruptFn(() => interrupt); |
| | const out = await donePromise; |
| | setOutput(out); |
| | setRunning(false); |
| | setShowStopBtn(false); |
| | }; |
| |
|
| | |
| | useEffect(() => { |
| | setCode(canvasData?.content ?? ''); |
| | runCode(canvasData?.content ?? ''); |
| | |
| | }, [canvasData?.content]); |
| |
|
| | if (canvasData?.type !== CanvasType.PY_INTERPRETER) { |
| | return null; |
| | } |
| |
|
| | return ( |
| | <div className="card bg-base-200 w-full h-full shadow-xl"> |
| | <div className="card-body"> |
| | <div className="flex justify-between items-center mb-4"> |
| | <span className="text-lg font-bold">Python Interpreter</span> |
| | <XCloseButton |
| | className="bg-base-100" |
| | onClick={() => setCanvasData(null)} |
| | /> |
| | </div> |
| | <div className="grid grid-rows-3 gap-4 h-full"> |
| | <textarea |
| | className="textarea textarea-bordered w-full h-full font-mono" |
| | value={code} |
| | onChange={(e) => setCode(e.target.value)} |
| | ></textarea> |
| | <div className="font-mono flex flex-col row-span-2"> |
| | <div className="flex items-center mb-2"> |
| | <button |
| | className="btn btn-sm bg-base-100" |
| | onClick={() => runCode(code)} |
| | disabled={running} |
| | > |
| | <PlayIcon className="h-6 w-6" /> Run |
| | </button> |
| | {showStopBtn && ( |
| | <button |
| | className="btn btn-sm bg-base-100 ml-2" |
| | onClick={() => interruptFn?.()} |
| | > |
| | <StopIcon className="h-6 w-6" /> Stop |
| | </button> |
| | )} |
| | <span className="grow text-right text-xs"> |
| | <OpenInNewTab href="https://github.com/ggerganov/llama.cpp/issues/11762"> |
| | Report a bug |
| | </OpenInNewTab> |
| | </span> |
| | </div> |
| | <textarea |
| | className="textarea textarea-bordered h-full dark-color" |
| | value={output} |
| | readOnly |
| | ></textarea> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | ); |
| | } |
| |
|