CodeBlock.svelte 12 KB

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