mirror of
https://github.com/open-webui/open-webui
synced 2025-05-23 14:24:22 +00:00
This adds `code_executions` as an array of code execution statuses to chat messages. The intent of this data is to be displayed in a similar manner as citations: at the bottom of the message, with buttons that open a modal for more info. However, code execution data doesn't fit well in citation modals, because they fundamentally differ in their formatting. Code execution status includes the code that was run (which benefits from being syntax-highlighted), and the output and generated files. This differs from citations which are just list of document names and links. Additionally, code execution is a process, whereas citations are only emitted once. This is why code execution data uses an ID-based approach, where each code execution instance is identified by a unique ID and can be updated by emitting a new `code_execution` message with the same ID. This allows the code execution status to be updated as code runs.
389 lines
8.9 KiB
Svelte
389 lines
8.9 KiB
Svelte
<script lang="ts">
|
|
import hljs from 'highlight.js';
|
|
import { loadPyodide } from 'pyodide';
|
|
import mermaid from 'mermaid';
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
import { getContext, getAllContexts, onMount, tick, createEventDispatcher } from 'svelte';
|
|
import { copyToClipboard } from '$lib/utils';
|
|
|
|
import 'highlight.js/styles/github-dark.min.css';
|
|
|
|
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
|
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
|
|
|
|
const i18n = getContext('i18n');
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let id = '';
|
|
export let save = false;
|
|
|
|
export let token;
|
|
export let lang = '';
|
|
export let code = '';
|
|
export let allow_execution = true;
|
|
|
|
let _code = '';
|
|
$: if (code) {
|
|
updateCode();
|
|
}
|
|
|
|
const updateCode = () => {
|
|
_code = code;
|
|
};
|
|
|
|
let _token = null;
|
|
|
|
let mermaidHtml = null;
|
|
|
|
let highlightedCode = null;
|
|
let executing = false;
|
|
|
|
let stdout = null;
|
|
let stderr = null;
|
|
let result = null;
|
|
|
|
let copied = false;
|
|
let saved = false;
|
|
|
|
const saveCode = () => {
|
|
saved = true;
|
|
|
|
code = _code;
|
|
dispatch('save', code);
|
|
|
|
setTimeout(() => {
|
|
saved = false;
|
|
}, 1000);
|
|
};
|
|
|
|
const copyCode = async () => {
|
|
copied = true;
|
|
await copyToClipboard(code);
|
|
|
|
setTimeout(() => {
|
|
copied = false;
|
|
}, 1000);
|
|
};
|
|
|
|
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 ',
|
|
' with '
|
|
];
|
|
|
|
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) => {
|
|
if (!code.includes('input') && !code.includes('matplotlib')) {
|
|
executePythonAsWorker(code);
|
|
} else {
|
|
result = null;
|
|
stdout = null;
|
|
stderr = null;
|
|
|
|
executing = true;
|
|
|
|
document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
|
|
|
|
let pyodide = await loadPyodide({
|
|
indexURL: '/pyodide/',
|
|
stdout: (text) => {
|
|
console.log('Python output:', text);
|
|
|
|
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']
|
|
});
|
|
|
|
try {
|
|
const micropip = pyodide.pyimport('micropip');
|
|
|
|
// await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
|
|
|
|
let packages = [
|
|
code.includes('requests') ? 'requests' : null,
|
|
code.includes('bs4') ? 'beautifulsoup4' : null,
|
|
code.includes('numpy') ? 'numpy' : null,
|
|
code.includes('pandas') ? 'pandas' : null,
|
|
code.includes('matplotlib') ? 'matplotlib' : null,
|
|
code.includes('sklearn') ? 'scikit-learn' : null,
|
|
code.includes('scipy') ? 'scipy' : null,
|
|
code.includes('re') ? 'regex' : null,
|
|
code.includes('seaborn') ? 'seaborn' : null
|
|
].filter(Boolean);
|
|
|
|
console.log(packages);
|
|
await micropip.install(packages);
|
|
|
|
result = await pyodide.runPythonAsync(`from js import prompt
|
|
def input(p):
|
|
return prompt(p)
|
|
__builtins__.input = input`);
|
|
|
|
result = await pyodide.runPython(code);
|
|
|
|
if (!result) {
|
|
result = '[NO OUTPUT]';
|
|
}
|
|
|
|
console.log(result);
|
|
console.log(stdout);
|
|
console.log(stderr);
|
|
|
|
const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
|
|
|
|
if (pltCanvasElement?.innerHTML !== '') {
|
|
pltCanvasElement.classList.add('pt-4');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
stderr = error;
|
|
}
|
|
|
|
executing = false;
|
|
}
|
|
};
|
|
|
|
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,
|
|
code.includes('pandas') ? 'pandas' : null,
|
|
code.includes('sklearn') ? 'scikit-learn' : null,
|
|
code.includes('scipy') ? 'scipy' : null,
|
|
code.includes('re') ? 'regex' : null,
|
|
code.includes('seaborn') ? 'seaborn' : null
|
|
].filter(Boolean);
|
|
|
|
console.log(packages);
|
|
|
|
const pyodideWorker = new PyodideWorker();
|
|
|
|
pyodideWorker.postMessage({
|
|
id: id,
|
|
code: code,
|
|
packages: packages
|
|
});
|
|
|
|
setTimeout(() => {
|
|
if (executing) {
|
|
executing = false;
|
|
stderr = 'Execution Time Limit Exceeded';
|
|
pyodideWorker.terminate();
|
|
}
|
|
}, 60000);
|
|
|
|
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;
|
|
};
|
|
|
|
pyodideWorker.onerror = (event) => {
|
|
console.log('pyodideWorker.onerror', event);
|
|
executing = false;
|
|
};
|
|
};
|
|
|
|
let debounceTimeout;
|
|
|
|
const drawMermaidDiagram = async () => {
|
|
try {
|
|
if (await mermaid.parse(code)) {
|
|
const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code);
|
|
mermaidHtml = svg;
|
|
}
|
|
} catch (error) {
|
|
console.log('Error:', error);
|
|
}
|
|
};
|
|
|
|
const render = async () => {
|
|
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
|
(async () => {
|
|
await drawMermaidDiagram();
|
|
})();
|
|
}
|
|
};
|
|
|
|
$: if (token) {
|
|
if (JSON.stringify(token) !== JSON.stringify(_token)) {
|
|
_token = token;
|
|
}
|
|
}
|
|
|
|
$: if (_token) {
|
|
render();
|
|
}
|
|
|
|
$: dispatch('code', { lang, code });
|
|
|
|
onMount(async () => {
|
|
console.log('codeblock', lang, code);
|
|
|
|
if (lang) {
|
|
dispatch('code', { lang, code });
|
|
}
|
|
if (document.documentElement.classList.contains('dark')) {
|
|
mermaid.initialize({
|
|
startOnLoad: true,
|
|
theme: 'dark',
|
|
securityLevel: 'loose'
|
|
});
|
|
} else {
|
|
mermaid.initialize({
|
|
startOnLoad: true,
|
|
theme: 'default',
|
|
securityLevel: 'loose'
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div>
|
|
<div class="relative my-2 flex flex-col rounded-lg" dir="ltr">
|
|
{#if lang === 'mermaid'}
|
|
{#if mermaidHtml}
|
|
<SvgPanZoom
|
|
className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
|
|
svg={mermaidHtml}
|
|
content={_token.text}
|
|
/>
|
|
{:else}
|
|
<pre class="mermaid">{code}</pre>
|
|
{/if}
|
|
{:else}
|
|
<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}
|
|
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
|
{:else if allow_execution}
|
|
<button
|
|
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"
|
|
on:click={async () => {
|
|
code = _code;
|
|
await tick();
|
|
executePython(code);
|
|
}}>{$i18n.t('Run')}</button
|
|
>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if save}
|
|
<button
|
|
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"
|
|
on:click={saveCode}
|
|
>
|
|
{saved ? $i18n.t('Saved') : $i18n.t('Save')}
|
|
</button>
|
|
{/if}
|
|
|
|
<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>
|
|
|
|
<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;
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
id="plt-canvas-{id}"
|
|
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
|
|
/>
|
|
|
|
{#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}
|
|
{/if}
|
|
</div>
|
|
</div>
|