|
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> |
|
); |
|
} |
|
|