CanvasPyInterpreter.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { useEffect, useState } from 'react';
  2. import { useAppContext } from '../utils/app.context';
  3. import { OpenInNewTab, XCloseButton } from '../utils/common';
  4. import { CanvasType } from '../utils/types';
  5. import { PlayIcon, StopIcon } from '@heroicons/react/24/outline';
  6. import StorageUtils from '../utils/storage';
  7. const canInterrupt = typeof SharedArrayBuffer === 'function';
  8. // adapted from https://pyodide.org/en/stable/usage/webworker.html
  9. const WORKER_CODE = `
  10. importScripts("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js");
  11. let stdOutAndErr = [];
  12. let pyodideReadyPromise = loadPyodide({
  13. stdout: (data) => stdOutAndErr.push(data),
  14. stderr: (data) => stdOutAndErr.push(data),
  15. });
  16. let alreadySetBuff = false;
  17. self.onmessage = async (event) => {
  18. stdOutAndErr = [];
  19. // make sure loading is done
  20. const pyodide = await pyodideReadyPromise;
  21. const { id, python, context, interruptBuffer } = event.data;
  22. if (interruptBuffer && !alreadySetBuff) {
  23. pyodide.setInterruptBuffer(interruptBuffer);
  24. alreadySetBuff = true;
  25. }
  26. // Now load any packages we need, run the code, and send the result back.
  27. await pyodide.loadPackagesFromImports(python);
  28. // make a Python dictionary with the data from content
  29. const dict = pyodide.globals.get("dict");
  30. const globals = dict(Object.entries(context));
  31. try {
  32. self.postMessage({ id, running: true });
  33. // Execute the python code in this context
  34. const result = pyodide.runPython(python, { globals });
  35. self.postMessage({ result, id, stdOutAndErr });
  36. } catch (error) {
  37. self.postMessage({ error: error.message, id });
  38. }
  39. interruptBuffer[0] = 0;
  40. };
  41. `;
  42. let worker: Worker;
  43. const interruptBuffer = canInterrupt
  44. ? new Uint8Array(new SharedArrayBuffer(1))
  45. : null;
  46. const startWorker = () => {
  47. if (!worker) {
  48. worker = new Worker(
  49. URL.createObjectURL(new Blob([WORKER_CODE], { type: 'text/javascript' }))
  50. );
  51. }
  52. };
  53. if (StorageUtils.getConfig().pyIntepreterEnabled) {
  54. startWorker();
  55. }
  56. const runCodeInWorker = (
  57. pyCode: string,
  58. callbackRunning: () => void
  59. ): {
  60. donePromise: Promise<string>;
  61. interrupt: () => void;
  62. } => {
  63. startWorker();
  64. const id = Math.random() * 1e8;
  65. const context = {};
  66. if (interruptBuffer) {
  67. interruptBuffer[0] = 0;
  68. }
  69. const donePromise = new Promise<string>((resolve) => {
  70. worker.onmessage = (event) => {
  71. const { error, stdOutAndErr, running } = event.data;
  72. if (id !== event.data.id) return;
  73. if (running) {
  74. callbackRunning();
  75. return;
  76. } else if (error) {
  77. resolve(error.toString());
  78. } else {
  79. resolve(stdOutAndErr.join('\n'));
  80. }
  81. };
  82. worker.postMessage({ id, python: pyCode, context, interruptBuffer });
  83. });
  84. const interrupt = () => {
  85. console.log('Interrupting...');
  86. console.trace();
  87. if (interruptBuffer) {
  88. interruptBuffer[0] = 2;
  89. }
  90. };
  91. return { donePromise, interrupt };
  92. };
  93. export default function CanvasPyInterpreter() {
  94. const { canvasData, setCanvasData } = useAppContext();
  95. const [code, setCode] = useState(canvasData?.content ?? ''); // copy to avoid direct mutation
  96. const [running, setRunning] = useState(false);
  97. const [output, setOutput] = useState('');
  98. const [interruptFn, setInterruptFn] = useState<() => void>();
  99. const [showStopBtn, setShowStopBtn] = useState(false);
  100. const runCode = async (pycode: string) => {
  101. interruptFn?.();
  102. setRunning(true);
  103. setOutput('Loading Pyodide...');
  104. const { donePromise, interrupt } = runCodeInWorker(pycode, () => {
  105. setOutput('Running...');
  106. setShowStopBtn(canInterrupt);
  107. });
  108. setInterruptFn(() => interrupt);
  109. const out = await donePromise;
  110. setOutput(out);
  111. setRunning(false);
  112. setShowStopBtn(false);
  113. };
  114. // run code on mount
  115. useEffect(() => {
  116. setCode(canvasData?.content ?? '');
  117. runCode(canvasData?.content ?? '');
  118. // eslint-disable-next-line react-hooks/exhaustive-deps
  119. }, [canvasData?.content]);
  120. if (canvasData?.type !== CanvasType.PY_INTERPRETER) {
  121. return null;
  122. }
  123. return (
  124. <div className="card bg-base-200 w-full h-full shadow-xl">
  125. <div className="card-body">
  126. <div className="flex justify-between items-center mb-4">
  127. <span className="text-lg font-bold">Python Interpreter</span>
  128. <XCloseButton
  129. className="bg-base-100"
  130. onClick={() => setCanvasData(null)}
  131. />
  132. </div>
  133. <div className="grid grid-rows-3 gap-4 h-full">
  134. <textarea
  135. className="textarea textarea-bordered w-full h-full font-mono"
  136. value={code}
  137. onChange={(e) => setCode(e.target.value)}
  138. ></textarea>
  139. <div className="font-mono flex flex-col row-span-2">
  140. <div className="flex items-center mb-2">
  141. <button
  142. className="btn btn-sm bg-base-100"
  143. onClick={() => runCode(code)}
  144. disabled={running}
  145. >
  146. <PlayIcon className="h-6 w-6" /> Run
  147. </button>
  148. {showStopBtn && (
  149. <button
  150. className="btn btn-sm bg-base-100 ml-2"
  151. onClick={() => interruptFn?.()}
  152. >
  153. <StopIcon className="h-6 w-6" /> Stop
  154. </button>
  155. )}
  156. <span className="grow text-right text-xs">
  157. <OpenInNewTab href="https://github.com/ggerganov/llama.cpp/issues/11762">
  158. Report a bug
  159. </OpenInNewTab>
  160. </span>
  161. </div>
  162. <textarea
  163. className="textarea textarea-bordered h-full dark-color"
  164. value={output}
  165. readOnly
  166. ></textarea>
  167. </div>
  168. </div>
  169. </div>
  170. </div>
  171. );
  172. }