CodeBlock.svelte 13 KB

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