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

608 lines
14 KiB
Svelte
Raw Normal View History

2024-01-22 19:33:49 +08:00
<script lang="ts">
2025-08-26 21:34:33 +08:00
import hljs from 'highlight.js';
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';
2025-02-22 17:22:17 +08:00
import { getContext, onMount, tick, onDestroy } 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-10-07 03:51:29 +08:00
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
2025-02-18 08:25:50 +08:00
import { config } from '$lib/stores';
import { executeCode } from '$lib/apis/utils';
import { toast } from 'svelte-sonner';
2025-03-04 13:10:15 +08:00
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
import CommandLine from '$lib/components/icons/CommandLine.svelte';
2025-05-17 01:47:43 +08:00
import Cube from '$lib/components/icons/Cube.svelte';
2024-05-17 11:49:28 +08:00
2024-08-08 20:24:47 +08:00
const i18n = getContext('i18n');
2024-05-17 11:49:28 +08:00
export let id = '';
2025-08-26 21:34:33 +08:00
export let edit = true;
2024-10-14 14:49:32 +08:00
2025-02-22 17:22:17 +08:00
export let onSave = (e) => {};
2025-05-17 07:14:26 +08:00
export let onUpdate = (e) => {};
2025-05-17 01:47:43 +08:00
export let onPreview = (e) => {};
2025-02-22 17:22:17 +08:00
2024-10-06 03:07:45 +08:00
export let save = false;
2024-10-14 14:49:32 +08:00
export let run = true;
2025-05-17 01:47:43 +08:00
export let preview = false;
2025-03-15 10:01:59 +08:00
export let collapsed = 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 = '';
2025-02-03 16:03:41 +08:00
export let attributes = {};
2024-10-14 14:49:32 +08:00
export let className = 'my-2';
2024-10-14 14:49:32 +08:00
export let editorClassName = '';
2025-08-10 05:03:02 +08:00
export let stickyButtonsClassName = 'top-0';
2024-01-22 19:33:49 +08:00
2025-02-10 14:43:32 +08:00
let pyodideWorker = null;
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;
2025-02-05 10:33:22 +08:00
let files = null;
2024-01-22 19:33:49 +08:00
let copied = false;
2024-10-06 03:04:36 +08:00
let saved = false;
2025-03-04 13:10:15 +08:00
const collapseCodeBlock = () => {
collapsed = !collapsed;
};
2024-10-06 03:04:36 +08:00
const saveCode = () => {
saved = true;
code = _code;
2025-02-22 17:22:17 +08:00
onSave(code);
2024-10-06 03:04:36 +08:00
setTimeout(() => {
saved = false;
}, 1000);
};
2024-01-22 19:33:49 +08:00
const copyCode = async () => {
copied = true;
2025-08-23 21:42:35 +08:00
await copyToClipboard(_code);
2024-01-22 19:33:49 +08:00
setTimeout(() => {
copied = false;
}, 1000);
};
2025-05-17 01:47:43 +08:00
const previewCode = () => {
onPreview(code);
};
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) => {
result = null;
stdout = null;
stderr = null;
executing = true;
2025-02-18 08:25:50 +08:00
if ($config?.code?.engine === 'jupyter') {
const output = await executeCode(localStorage.token, code).catch((error) => {
toast.error(`${error}`);
return null;
});
if (output) {
2025-02-19 12:15:16 +08:00
if (output['stdout']) {
stdout = output['stdout'];
const stdoutLines = stdout.split('\n');
for (const [idx, line] of stdoutLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
if (stdout.startsWith(`${line}\n`)) {
stdout = stdout.replace(`${line}\n`, ``);
} else if (stdout.startsWith(`${line}`)) {
stdout = stdout.replace(`${line}`, ``);
}
}
}
}
if (output['result']) {
result = output['result'];
const resultLines = result.split('\n');
for (const [idx, line] of resultLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
if (result.startsWith(`${line}\n`)) {
result = result.replace(`${line}\n`, ``);
} else if (result.startsWith(`${line}`)) {
result = result.replace(`${line}`, ``);
}
}
}
}
output['stderr'] && (stderr = output['stderr']);
2025-02-18 08:25:50 +08:00
}
executing = false;
2025-02-18 08:25:50 +08:00
} else {
executePythonAsWorker(code);
}
2024-05-17 16:39:07 +08:00
};
const executePythonAsWorker = async (code) => {
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,
2025-02-04 14:07:59 +08:00
code.includes('seaborn') ? 'seaborn' : null,
code.includes('sympy') ? 'sympy' : null,
2025-02-05 10:33:22 +08:00
code.includes('tiktoken') ? 'tiktoken' : null,
2025-02-05 15:44:51 +08:00
code.includes('matplotlib') ? 'matplotlib' : null,
code.includes('pytz') ? 'pytz' : null,
code.includes('openai') ? 'openai' : null
2024-05-17 16:39:07 +08:00
].filter(Boolean);
2024-05-19 05:29:39 +08:00
console.log(packages);
2025-02-10 14:43:32 +08:00
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);
2025-02-05 10:33:22 +08:00
if (data['stdout']) {
stdout = data['stdout'];
const stdoutLines = stdout.split('\n');
for (const [idx, line] of stdoutLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
2025-02-19 12:15:16 +08:00
if (stdout.startsWith(`${line}\n`)) {
stdout = stdout.replace(`${line}\n`, ``);
} else if (stdout.startsWith(`${line}`)) {
stdout = stdout.replace(`${line}`, ``);
}
}
}
}
if (data['result']) {
result = data['result'];
const resultLines = result.split('\n');
for (const [idx, line] of resultLines.entries()) {
if (line.startsWith('data:image/png;base64')) {
if (files) {
files.push({
type: 'image/png',
data: line
});
} else {
files = [
{
type: 'image/png',
data: line
}
];
}
if (result.startsWith(`${line}\n`)) {
result = result.replace(`${line}\n`, ``);
} else if (result.startsWith(`${line}`)) {
result = result.replace(`${line}`, ``);
}
2025-02-05 10:33:22 +08:00
}
}
}
2024-05-17 16:39:07 +08:00
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
}
2025-05-17 07:14:26 +08:00
onUpdate(token);
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
2025-02-03 16:03:41 +08:00
$: if (attributes) {
onAttributesUpdate();
}
const onAttributesUpdate = () => {
if (attributes?.output) {
// Create a helper function to unescape HTML entities
const unescapeHtml = (html) => {
const textArea = document.createElement('textarea');
textArea.innerHTML = html;
return textArea.value;
};
try {
// Unescape the HTML-encoded string
const unescapedOutput = unescapeHtml(attributes.output);
// Parse the unescaped string into JSON
const output = JSON.parse(unescapedOutput);
// Assign the parsed values to variables
stdout = output.stdout;
stderr = output.stderr;
result = output.result;
} catch (error) {
console.error('Error:', error);
}
}
};
2024-08-20 01:42:31 +08:00
onMount(async () => {
2025-05-17 07:14:26 +08:00
if (token) {
onUpdate(token);
2024-10-06 15:30:50 +08:00
}
2025-05-17 07:14:26 +08:00
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'
});
}
});
2025-02-10 14:43:32 +08:00
onDestroy(() => {
if (pyodideWorker) {
pyodideWorker.terminate();
}
});
2024-01-22 19:33:49 +08:00
</script>
2024-10-06 11:48:55 +08:00
<div>
2024-10-14 14:49:32 +08:00
<div class="relative {className} flex flex-col rounded-lg" dir="ltr">
2024-10-06 11:48:55 +08:00
{#if lang === 'mermaid'}
{#if mermaidHtml}
2024-10-07 03:51:29 +08:00
<SvgPanZoom
2025-02-16 11:50:40 +08:00
className=" border border-gray-100 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
2024-10-07 03:51:29 +08:00
svg={mermaidHtml}
2024-10-11 15:10:00 +08:00
content={_token.text}
2024-10-07 03:51:29 +08:00
/>
2024-10-06 11:48:55 +08:00
{: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
2024-10-14 14:49:32 +08:00
class="sticky {stickyButtonsClassName} mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
2024-10-06 11:48:55 +08:00
>
<div class="flex items-center gap-0.5 translate-y-[1px]">
2025-03-04 13:10:15 +08:00
<button
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"
on:click={collapseCodeBlock}
>
2025-05-17 01:47:43 +08:00
<div class=" -translate-y-[0.5px]">
2025-03-04 13:10:15 +08:00
<ChevronUpDown className="size-3" />
</div>
<div>
{collapsed ? $i18n.t('Expand') : $i18n.t('Collapse')}
</div>
</button>
2025-05-17 01:47:43 +08:00
{#if preview && ['html', 'svg'].includes(lang)}
<button
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"
on:click={previewCode}
>
<div class=" -translate-y-[0.5px]">
<Cube className="size-3" />
</div>
<div>
{$i18n.t('Preview')}
</div>
</button>
{/if}
{#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))}
2024-10-06 11:48:55 +08:00
{#if executing}
2025-03-07 19:59:09 +08:00
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">
{$i18n.t('Running')}
</div>
2024-10-14 14:49:32 +08:00
{:else if run}
2024-10-06 11:48:55 +08:00
<button
2025-03-04 13:10:15 +08:00
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"
2024-10-06 11:48:55 +08:00
on:click={async () => {
code = _code;
await tick();
executePython(code);
2025-03-04 13:10:15 +08:00
}}
2024-10-06 11:48:55 +08:00
>
2025-05-17 01:47:43 +08:00
<div class=" -translate-y-[0.5px]">
2025-03-04 13:10:15 +08:00
<CommandLine className="size-3" />
</div>
<div>
{$i18n.t('Run')}
</div>
</button>
2024-10-06 11:48:55 +08:00
{/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
2024-10-14 14:49:32 +08:00
class="language-{lang} rounded-t-lg -mt-8 {editorClassName
? editorClassName
: executing || stdout || stderr || result
? ''
: 'rounded-b-lg'} overflow-hidden"
2024-10-06 11:48:55 +08:00
>
<div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
2025-03-04 13:10:15 +08:00
{#if !collapsed}
2025-08-26 21:34:33 +08:00
{#if edit}
<CodeEditor
value={code}
{id}
{lang}
onSave={() => {
saveCode();
}}
onChange={(value) => {
_code = value;
}}
/>
{:else}
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre text-sm"
>{@html hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value ||
code}</code
></pre>
{/if}
2025-03-04 13:10:15 +08:00
{:else}
<div
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"
>
<span class="text-gray-500 italic">
{$i18n.t('{{COUNT}} hidden lines', {
COUNT: code.split('\n').length
})}
</span>
</div>
{/if}
</div>
2024-10-06 11:48:55 +08:00
2025-03-04 13:10:15 +08:00
{#if !collapsed}
2025-02-04 06:16:22 +08:00
<div
2025-03-04 13:10:15 +08:00
id="plt-canvas-{id}"
class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing || stdout || stderr || result || files}
<div
class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
>
{#if executing}
2025-02-03 08:56:21 +08:00
<div class=" ">
2025-08-20 02:39:17 +08:00
<div class=" text-gray-500 text-xs mb-1">{$i18n.t('STDOUT/STDERR')}</div>
<div class="text-sm">{$i18n.t('Running...')}</div>
2025-02-03 08:56:21 +08:00
</div>
2025-03-04 13:10:15 +08:00
{:else}
{#if stdout || stderr}
<div class=" ">
2025-08-20 02:39:17 +08:00
<div class=" text-gray-500 text-xs mb-1">{$i18n.t('STDOUT/STDERR')}</div>
2025-03-04 13:10:15 +08:00
<div
class="text-sm {stdout?.split('\n')?.length > 100
? `max-h-96`
: ''} overflow-y-auto"
>
{stdout || stderr}
2025-02-05 10:33:22 +08:00
</div>
2025-03-04 13:10:15 +08:00
</div>
{/if}
{#if result || files}
<div class=" ">
2025-08-20 02:39:17 +08:00
<div class=" text-gray-500 text-xs mb-1">{$i18n.t('RESULT')}</div>
2025-03-04 13:10:15 +08:00
{#if result}
<div class="text-sm">{`${JSON.stringify(result)}`}</div>
{/if}
{#if files}
<div class="flex flex-col gap-2">
{#each files as file}
{#if file.type.startsWith('image')}
<img src={file.data} alt="Output" class=" w-full max-w-[36rem]" />
{/if}
{/each}
</div>
{/if}
</div>
{/if}
2025-02-03 08:56:21 +08:00
{/if}
2025-03-04 13:10:15 +08:00
</div>
{/if}
2024-10-06 11:48:55 +08:00
{/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>