CodeBlock.svelte 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. <script lang="ts">
  2. import hljs from 'highlight.js';
  3. import { loadPyodide } from 'pyodide';
  4. import mermaid from 'mermaid';
  5. import { v4 as uuidv4 } from 'uuid';
  6. import {
  7. getContext,
  8. getAllContexts,
  9. onMount,
  10. tick,
  11. createEventDispatcher,
  12. onDestroy
  13. } from 'svelte';
  14. import { copyToClipboard } from '$lib/utils';
  15. import 'highlight.js/styles/github-dark.min.css';
  16. import PyodideWorker from '$lib/workers/pyodide.worker?worker';
  17. import CodeEditor from '$lib/components/common/CodeEditor.svelte';
  18. import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
  19. const i18n = getContext('i18n');
  20. const dispatch = createEventDispatcher();
  21. export let id = '';
  22. export let save = false;
  23. export let run = true;
  24. export let token;
  25. export let lang = '';
  26. export let code = '';
  27. export let attributes = {};
  28. export let className = 'my-2';
  29. export let editorClassName = '';
  30. export let stickyButtonsClassName = 'top-8';
  31. let pyodideWorker = null;
  32. let _code = '';
  33. $: if (code) {
  34. updateCode();
  35. }
  36. const updateCode = () => {
  37. _code = code;
  38. };
  39. let _token = null;
  40. let mermaidHtml = null;
  41. let highlightedCode = null;
  42. let executing = false;
  43. let stdout = null;
  44. let stderr = null;
  45. let result = null;
  46. let files = null;
  47. let copied = false;
  48. let saved = false;
  49. const saveCode = () => {
  50. saved = true;
  51. code = _code;
  52. dispatch('save', code);
  53. setTimeout(() => {
  54. saved = false;
  55. }, 1000);
  56. };
  57. const copyCode = async () => {
  58. copied = true;
  59. await copyToClipboard(code);
  60. setTimeout(() => {
  61. copied = false;
  62. }, 1000);
  63. };
  64. const checkPythonCode = (str) => {
  65. // Check if the string contains typical Python syntax characters
  66. const pythonSyntax = [
  67. 'def ',
  68. 'else:',
  69. 'elif ',
  70. 'try:',
  71. 'except:',
  72. 'finally:',
  73. 'yield ',
  74. 'lambda ',
  75. 'assert ',
  76. 'nonlocal ',
  77. 'del ',
  78. 'True',
  79. 'False',
  80. 'None',
  81. ' and ',
  82. ' or ',
  83. ' not ',
  84. ' in ',
  85. ' is ',
  86. ' with '
  87. ];
  88. for (let syntax of pythonSyntax) {
  89. if (str.includes(syntax)) {
  90. return true;
  91. }
  92. }
  93. // If none of the above conditions met, it's probably not Python code
  94. return false;
  95. };
  96. const executePython = async (code) => {
  97. executePythonAsWorker(code);
  98. };
  99. const executePythonAsWorker = async (code) => {
  100. result = null;
  101. stdout = null;
  102. stderr = null;
  103. executing = true;
  104. let packages = [
  105. code.includes('requests') ? 'requests' : null,
  106. code.includes('bs4') ? 'beautifulsoup4' : null,
  107. code.includes('numpy') ? 'numpy' : null,
  108. code.includes('pandas') ? 'pandas' : null,
  109. code.includes('sklearn') ? 'scikit-learn' : null,
  110. code.includes('scipy') ? 'scipy' : null,
  111. code.includes('re') ? 'regex' : null,
  112. code.includes('seaborn') ? 'seaborn' : null,
  113. code.includes('sympy') ? 'sympy' : null,
  114. code.includes('tiktoken') ? 'tiktoken' : null,
  115. code.includes('matplotlib') ? 'matplotlib' : null,
  116. code.includes('pytz') ? 'pytz' : null
  117. ].filter(Boolean);
  118. console.log(packages);
  119. pyodideWorker = new PyodideWorker();
  120. pyodideWorker.postMessage({
  121. id: id,
  122. code: code,
  123. packages: packages
  124. });
  125. setTimeout(() => {
  126. if (executing) {
  127. executing = false;
  128. stderr = 'Execution Time Limit Exceeded';
  129. pyodideWorker.terminate();
  130. }
  131. }, 60000);
  132. pyodideWorker.onmessage = (event) => {
  133. console.log('pyodideWorker.onmessage', event);
  134. const { id, ...data } = event.data;
  135. console.log(id, data);
  136. if (data['stdout']) {
  137. stdout = data['stdout'];
  138. const stdoutLines = stdout.split('\n');
  139. for (const [idx, line] of stdoutLines.entries()) {
  140. if (line.startsWith('data:image/png;base64')) {
  141. if (files) {
  142. files.push({
  143. type: 'image/png',
  144. data: line
  145. });
  146. } else {
  147. files = [
  148. {
  149. type: 'image/png',
  150. data: line
  151. }
  152. ];
  153. }
  154. stdout = stdout.replace(`${line}\n`, ``);
  155. }
  156. }
  157. }
  158. data['stderr'] && (stderr = data['stderr']);
  159. data['result'] && (result = data['result']);
  160. executing = false;
  161. };
  162. pyodideWorker.onerror = (event) => {
  163. console.log('pyodideWorker.onerror', event);
  164. executing = false;
  165. };
  166. };
  167. let debounceTimeout;
  168. const drawMermaidDiagram = async () => {
  169. try {
  170. if (await mermaid.parse(code)) {
  171. const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code);
  172. mermaidHtml = svg;
  173. }
  174. } catch (error) {
  175. console.log('Error:', error);
  176. }
  177. };
  178. const render = async () => {
  179. if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
  180. (async () => {
  181. await drawMermaidDiagram();
  182. })();
  183. }
  184. };
  185. $: if (token) {
  186. if (JSON.stringify(token) !== JSON.stringify(_token)) {
  187. _token = token;
  188. }
  189. }
  190. $: if (_token) {
  191. render();
  192. }
  193. $: dispatch('code', { lang, code });
  194. $: if (attributes) {
  195. onAttributesUpdate();
  196. }
  197. const onAttributesUpdate = () => {
  198. if (attributes?.output) {
  199. // Create a helper function to unescape HTML entities
  200. const unescapeHtml = (html) => {
  201. const textArea = document.createElement('textarea');
  202. textArea.innerHTML = html;
  203. return textArea.value;
  204. };
  205. try {
  206. // Unescape the HTML-encoded string
  207. const unescapedOutput = unescapeHtml(attributes.output);
  208. // Parse the unescaped string into JSON
  209. const output = JSON.parse(unescapedOutput);
  210. // Assign the parsed values to variables
  211. stdout = output.stdout;
  212. stderr = output.stderr;
  213. result = output.result;
  214. } catch (error) {
  215. console.error('Error:', error);
  216. }
  217. }
  218. };
  219. onMount(async () => {
  220. console.log('codeblock', lang, code);
  221. if (lang) {
  222. dispatch('code', { lang, code });
  223. }
  224. if (document.documentElement.classList.contains('dark')) {
  225. mermaid.initialize({
  226. startOnLoad: true,
  227. theme: 'dark',
  228. securityLevel: 'loose'
  229. });
  230. } else {
  231. mermaid.initialize({
  232. startOnLoad: true,
  233. theme: 'default',
  234. securityLevel: 'loose'
  235. });
  236. }
  237. });
  238. onDestroy(() => {
  239. if (pyodideWorker) {
  240. pyodideWorker.terminate();
  241. }
  242. });
  243. </script>
  244. <div>
  245. <div class="relative {className} flex flex-col rounded-lg" dir="ltr">
  246. {#if lang === 'mermaid'}
  247. {#if mermaidHtml}
  248. <SvgPanZoom
  249. className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
  250. svg={mermaidHtml}
  251. content={_token.text}
  252. />
  253. {:else}
  254. <pre class="mermaid">{code}</pre>
  255. {/if}
  256. {:else}
  257. <div class="text-text-300 absolute pl-4 py-1.5 text-xs font-medium dark:text-white">
  258. {lang}
  259. </div>
  260. <div
  261. class="sticky {stickyButtonsClassName} mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
  262. >
  263. <div class="flex items-center gap-0.5 translate-y-[1px]">
  264. {#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
  265. {#if executing}
  266. <div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
  267. {:else if run}
  268. <button
  269. class="run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
  270. on:click={async () => {
  271. code = _code;
  272. await tick();
  273. executePython(code);
  274. }}>{$i18n.t('Run')}</button
  275. >
  276. {/if}
  277. {/if}
  278. {#if save}
  279. <button
  280. class="save-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
  281. on:click={saveCode}
  282. >
  283. {saved ? $i18n.t('Saved') : $i18n.t('Save')}
  284. </button>
  285. {/if}
  286. <button
  287. class="copy-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
  288. on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
  289. >
  290. </div>
  291. </div>
  292. <div
  293. class="language-{lang} rounded-t-lg -mt-8 {editorClassName
  294. ? editorClassName
  295. : executing || stdout || stderr || result
  296. ? ''
  297. : 'rounded-b-lg'} overflow-hidden"
  298. >
  299. <div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
  300. <CodeEditor
  301. value={code}
  302. {id}
  303. {lang}
  304. on:save={() => {
  305. saveCode();
  306. }}
  307. on:change={(e) => {
  308. _code = e.detail.value;
  309. }}
  310. />
  311. </div>
  312. <div
  313. id="plt-canvas-{id}"
  314. class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
  315. />
  316. {#if executing || stdout || stderr || result}
  317. <div
  318. class="bg-gray-50 dark:bg-[#202123] dark:text-white !rounded-b-lg py-4 px-4 flex flex-col gap-2"
  319. >
  320. {#if executing}
  321. <div class=" ">
  322. <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
  323. <div class="text-sm">Running...</div>
  324. </div>
  325. {:else}
  326. {#if stdout || stderr}
  327. <div class=" ">
  328. <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
  329. <div class="text-sm">{stdout || stderr}</div>
  330. </div>
  331. {/if}
  332. {#if result || files}
  333. <div class=" ">
  334. <div class=" text-gray-500 text-xs mb-1">RESULT</div>
  335. {#if result}
  336. <div class="text-sm">{`${JSON.stringify(result)}`}</div>
  337. {/if}
  338. {#if files}
  339. <div class="flex flex-col gap-2">
  340. {#each files as file}
  341. {#if file.type.startsWith('image')}
  342. <img src={file.data} alt="Output" />
  343. {/if}
  344. {/each}
  345. </div>
  346. {/if}
  347. </div>
  348. {/if}
  349. {/if}
  350. </div>
  351. {/if}
  352. {/if}
  353. </div>
  354. </div>