open-webui/src/lib/components/chat/Messages/CodeBlock.svelte

381 lines
8.6 KiB
Svelte
Raw Normal View History

2024-01-22 19:33:49 +08:00
<script lang="ts">
import hljs from 'highlight.js';
import { loadPyodide } from 'pyodide';
2024-08-14 23:34:44 +08:00
import mermaid from 'mermaid';
2024-08-19 23:44:14 +08:00
import { v4 as uuidv4 } from 'uuid';
2024-10-06 03:04:36 +08:00
import { getContext, getAllContexts, onMount, tick, createEventDispatcher } from 'svelte';
2024-08-08 20:24:47 +08:00
import { copyToClipboard } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css';
2024-05-19 20:19:48 +08:00
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
2024-10-06 03:04:36 +08:00
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
2024-05-17 11:49:28 +08:00
2024-08-08 20:24:47 +08:00
const i18n = getContext('i18n');
2024-10-06 03:04:36 +08:00
const dispatch = createEventDispatcher();
2024-08-08 20:24:47 +08:00
2024-05-17 11:49:28 +08:00
export let id = '';
2024-10-06 03:07:45 +08:00
export let save = false;
2024-01-22 19:33:49 +08:00
2024-08-14 23:34:44 +08:00
export let token;
2024-01-22 19:33:49 +08:00
export let lang = '';
export let code = '';
2024-10-06 03:04:36 +08:00
let _code = '';
$: if (code) {
updateCode();
}
const updateCode = () => {
_code = code;
};
2024-09-24 05:39:33 +08:00
let _token = null;
let mermaidHtml = null;
2024-05-25 17:47:09 +08:00
let highlightedCode = null;
let executing = false;
let stdout = null;
let stderr = null;
let result = null;
2024-01-22 19:33:49 +08:00
let copied = false;
2024-10-06 03:04:36 +08:00
let saved = false;
const saveCode = () => {
saved = true;
code = _code;
dispatch('save', code);
setTimeout(() => {
saved = false;
}, 1000);
};
2024-01-22 19:33:49 +08:00
const copyCode = async () => {
copied = true;
await copyToClipboard(code);
setTimeout(() => {
copied = false;
}, 1000);
};
2024-05-17 12:05:43 +08:00
const checkPythonCode = (str) => {
// Check if the string contains typical Python syntax characters
const pythonSyntax = [
'def ',
'else:',
'elif ',
'try:',
'except:',
'finally:',
'yield ',
'lambda ',
'assert ',
'nonlocal ',
'del ',
'True',
'False',
'None',
' and ',
' or ',
' not ',
' in ',
' is ',
2024-05-20 01:07:43 +08:00
' with '
2024-05-17 12:05:43 +08:00
];
for (let syntax of pythonSyntax) {
if (str.includes(syntax)) {
return true;
}
}
// If none of the above conditions met, it's probably not Python code
return false;
};
const executePython = async (code) => {
2024-05-17 16:54:37 +08:00
if (!code.includes('input') && !code.includes('matplotlib')) {
2024-05-17 16:39:07 +08:00
executePythonAsWorker(code);
} else {
result = null;
stdout = null;
stderr = null;
2024-05-17 13:22:10 +08:00
2024-05-17 16:39:07 +08:00
executing = true;
2024-05-17 11:49:28 +08:00
2024-05-17 17:15:39 +08:00
document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
2024-05-17 16:54:37 +08:00
2024-05-17 16:39:07 +08:00
let pyodide = await loadPyodide({
indexURL: '/pyodide/',
stdout: (text) => {
console.log('Python output:', text);
2024-05-17 16:39:07 +08:00
if (stdout) {
stdout += `${text}\n`;
} else {
stdout = `${text}\n`;
}
},
stderr: (text) => {
console.log('An error occured:', text);
if (stderr) {
stderr += `${text}\n`;
} else {
stderr = `${text}\n`;
}
},
packages: ['micropip']
2024-05-17 16:39:07 +08:00
});
2024-05-17 11:49:28 +08:00
2024-05-17 16:39:07 +08:00
try {
const micropip = pyodide.pyimport('micropip');
2024-05-17 14:35:25 +08:00
2024-05-21 10:20:49 +08:00
// await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
2024-05-17 14:25:55 +08:00
2024-05-17 16:39:07 +08:00
let packages = [
code.includes('requests') ? 'requests' : null,
code.includes('bs4') ? 'beautifulsoup4' : null,
code.includes('numpy') ? 'numpy' : null,
2024-05-17 16:54:37 +08:00
code.includes('pandas') ? 'pandas' : null,
2024-05-19 05:28:16 +08:00
code.includes('matplotlib') ? 'matplotlib' : null,
2024-05-19 05:29:39 +08:00
code.includes('sklearn') ? 'scikit-learn' : null,
2024-05-19 05:28:16 +08:00
code.includes('scipy') ? 'scipy' : null,
code.includes('re') ? 'regex' : null,
code.includes('seaborn') ? 'seaborn' : null
2024-05-17 16:39:07 +08:00
].filter(Boolean);
2024-05-17 14:25:55 +08:00
2024-05-17 16:39:07 +08:00
console.log(packages);
await micropip.install(packages);
2024-05-17 14:25:55 +08:00
2024-05-17 16:39:07 +08:00
result = await pyodide.runPythonAsync(`from js import prompt
2024-05-17 15:09:53 +08:00
def input(p):
return prompt(p)
__builtins__.input = input`);
2024-05-17 16:39:07 +08:00
result = await pyodide.runPython(code);
if (!result) {
result = '[NO OUTPUT]';
}
2024-05-17 13:30:09 +08:00
2024-05-17 16:39:07 +08:00
console.log(result);
console.log(stdout);
console.log(stderr);
2024-05-17 17:19:31 +08:00
const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
if (pltCanvasElement?.innerHTML !== '') {
2024-05-17 17:20:21 +08:00
pltCanvasElement.classList.add('pt-4');
2024-05-17 17:19:31 +08:00
}
2024-05-17 16:39:07 +08:00
} catch (error) {
console.error('Error:', error);
stderr = error;
2024-05-17 14:53:50 +08:00
}
2024-05-17 16:39:07 +08:00
executing = false;
2024-05-17 13:30:09 +08:00
}
2024-05-17 16:39:07 +08:00
};
const executePythonAsWorker = async (code) => {
result = null;
stdout = null;
stderr = null;
executing = true;
let packages = [
code.includes('requests') ? 'requests' : null,
code.includes('bs4') ? 'beautifulsoup4' : null,
code.includes('numpy') ? 'numpy' : null,
2024-05-17 16:54:37 +08:00
code.includes('pandas') ? 'pandas' : null,
2024-05-19 05:29:39 +08:00
code.includes('sklearn') ? 'scikit-learn' : null,
2024-05-19 05:28:16 +08:00
code.includes('scipy') ? 'scipy' : null,
code.includes('re') ? 'regex' : null,
code.includes('seaborn') ? 'seaborn' : null
2024-05-17 16:39:07 +08:00
].filter(Boolean);
2024-05-19 05:29:39 +08:00
console.log(packages);
const pyodideWorker = new PyodideWorker();
2024-05-17 16:39:07 +08:00
pyodideWorker.postMessage({
id: id,
code: code,
packages: packages
});
setTimeout(() => {
if (executing) {
executing = false;
stderr = 'Execution Time Limit Exceeded';
pyodideWorker.terminate();
}
2024-05-17 16:39:46 +08:00
}, 60000);
2024-05-17 16:39:07 +08:00
pyodideWorker.onmessage = (event) => {
console.log('pyodideWorker.onmessage', event);
const { id, ...data } = event.data;
console.log(id, data);
data['stdout'] && (stdout = data['stdout']);
data['stderr'] && (stderr = data['stderr']);
data['result'] && (result = data['result']);
executing = false;
};
2024-05-17 16:39:07 +08:00
pyodideWorker.onerror = (event) => {
console.log('pyodideWorker.onerror', event);
executing = false;
};
2024-05-17 11:49:28 +08:00
};
2024-06-21 03:27:34 +08:00
let debounceTimeout;
2024-08-19 23:28:38 +08:00
const drawMermaidDiagram = async () => {
try {
2024-09-03 21:39:09 +08:00
if (await mermaid.parse(code)) {
const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code);
mermaidHtml = svg;
}
2024-08-19 23:28:38 +08:00
} catch (error) {
2024-08-19 23:44:14 +08:00
console.log('Error:', error);
2024-08-19 23:28:38 +08:00
}
};
2024-09-24 05:39:33 +08:00
const render = async () => {
2024-08-19 23:44:14 +08:00
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
(async () => {
2024-08-19 23:44:14 +08:00
await drawMermaidDiagram();
})();
2024-08-14 23:34:44 +08:00
}
2024-09-24 05:39:33 +08:00
};
$: if (token) {
if (JSON.stringify(token) !== JSON.stringify(_token)) {
_token = token;
}
}
$: if (_token) {
render();
2024-05-25 17:47:09 +08:00
}
2024-08-20 01:42:31 +08:00
2024-10-06 15:28:33 +08:00
$: if (lang) {
dispatch('code', { lang });
}
2024-08-20 01:42:31 +08:00
onMount(async () => {
2024-09-23 20:24:50 +08:00
console.log('codeblock', lang, code);
2024-08-20 01:42:31 +08:00
if (document.documentElement.classList.contains('dark')) {
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose'
});
} else {
mermaid.initialize({
startOnLoad: true,
theme: 'default',
securityLevel: 'loose'
});
}
});
2024-01-22 19:33:49 +08:00
</script>
2024-10-06 11:48:55 +08:00
<div>
<div class="relative my-2 flex flex-col rounded-lg" dir="ltr">
{#if lang === 'mermaid'}
{#if mermaidHtml}
{@html `${mermaidHtml}`}
{:else}
<pre class="mermaid">{code}</pre>
{/if}
{:else}
2024-10-06 11:48:55 +08:00
<div class="text-text-300 absolute pl-4 py-1.5 text-xs font-medium dark:text-white">
{lang}
</div>
<div
class="sticky top-8 mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
>
<div class="flex items-center gap-0.5 translate-y-[1px]">
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
{#if executing}
2024-10-06 11:50:58 +08:00
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
2024-10-06 11:48:55 +08:00
{:else}
<button
2024-10-06 11:50:58 +08:00
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"
2024-10-06 11:48:55 +08:00
on:click={async () => {
code = _code;
await tick();
executePython(code);
}}>{$i18n.t('Run')}</button
>
{/if}
{/if}
{#if save}
2024-08-14 22:27:55 +08:00
<button
2024-10-06 11:50:58 +08:00
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"
2024-10-06 11:48:55 +08:00
on:click={saveCode}
2024-08-14 22:27:55 +08:00
>
2024-10-06 11:48:55 +08:00
{saved ? $i18n.t('Saved') : $i18n.t('Save')}
</button>
2024-08-14 22:27:55 +08:00
{/if}
2024-10-06 03:04:36 +08:00
2024-10-06 11:48:55 +08:00
<button
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"
on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
>
</div>
</div>
2024-10-06 03:04:36 +08:00
2024-10-06 11:48:55 +08:00
<div
class="language-{lang} rounded-t-lg -mt-8 {executing || stdout || stderr || result
? ''
: 'rounded-b-lg'} overflow-hidden"
>
<div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
<CodeEditor
value={code}
{id}
{lang}
on:save={() => {
saveCode();
}}
on:change={(e) => {
_code = e.detail.value;
}}
/>
2024-08-14 22:27:55 +08:00
</div>
2024-10-06 11:48:55 +08:00
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
2024-10-06 03:04:36 +08:00
/>
2024-10-06 11:48:55 +08:00
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div>
{/if}
2024-08-14 22:27:55 +08:00
{/if}
2024-10-06 11:48:55 +08:00
</div>
2024-05-25 17:47:09 +08:00
</div>