mirror of
https://github.com/open-webui/open-webui
synced 2025-03-19 11:38:22 +00:00
feat: editable code block
This commit is contained in:
parent
82ac2e4edb
commit
81440460f2
@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { getContext, getAllContexts, onMount } from 'svelte';
|
import { getContext, getAllContexts, onMount, tick, createEventDispatcher } from 'svelte';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
import 'highlight.js/styles/github-dark.min.css';
|
import 'highlight.js/styles/github-dark.min.css';
|
||||||
|
|
||||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||||
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
|
|
||||||
@ -20,6 +22,15 @@
|
|||||||
export let lang = '';
|
export let lang = '';
|
||||||
export let code = '';
|
export let code = '';
|
||||||
|
|
||||||
|
let _code = '';
|
||||||
|
$: if (code) {
|
||||||
|
updateCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCode = () => {
|
||||||
|
_code = code;
|
||||||
|
};
|
||||||
|
|
||||||
let _token = null;
|
let _token = null;
|
||||||
|
|
||||||
let mermaidHtml = null;
|
let mermaidHtml = null;
|
||||||
@ -32,6 +43,18 @@
|
|||||||
let result = null;
|
let result = null;
|
||||||
|
|
||||||
let copied = false;
|
let copied = false;
|
||||||
|
let saved = false;
|
||||||
|
|
||||||
|
const saveCode = () => {
|
||||||
|
saved = true;
|
||||||
|
|
||||||
|
code = _code;
|
||||||
|
dispatch('save', code);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
saved = false;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
const copyCode = async () => {
|
const copyCode = async () => {
|
||||||
copied = true;
|
copied = true;
|
||||||
@ -233,22 +256,11 @@ __builtins__.input = input`);
|
|||||||
(async () => {
|
(async () => {
|
||||||
await drawMermaidDiagram();
|
await drawMermaidDiagram();
|
||||||
})();
|
})();
|
||||||
} else {
|
|
||||||
// Function to perform the code highlighting
|
|
||||||
const highlightCode = () => {
|
|
||||||
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear the previous timeout if it exists
|
|
||||||
clearTimeout(debounceTimeout);
|
|
||||||
// Set a new timeout to debounce the code highlighting
|
|
||||||
debounceTimeout = setTimeout(highlightCode, 10);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (token) {
|
$: if (token) {
|
||||||
if (JSON.stringify(token) !== JSON.stringify(_token)) {
|
if (JSON.stringify(token) !== JSON.stringify(_token)) {
|
||||||
console.log('hi');
|
|
||||||
_token = token;
|
_token = token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,28 +307,50 @@ __builtins__.input = input`);
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="copy-code-button bg-none border-none p-1"
|
class="copy-code-button bg-none border-none p-1"
|
||||||
on:click={() => {
|
on:click={async () => {
|
||||||
|
code = _code;
|
||||||
|
await tick();
|
||||||
executePython(code);
|
executePython(code);
|
||||||
}}>{$i18n.t('Run')}</button
|
}}>{$i18n.t('Run')}</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<button class="copy-code-button bg-none border-none p-1" on:click={saveCode}>
|
||||||
|
{saved ? $i18n.t('Saved') : $i18n.t('Save')}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
|
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
|
||||||
>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre
|
<div
|
||||||
|
class="language-{lang} rounded-t-none {executing || stdout || stderr || result
|
||||||
|
? ''
|
||||||
|
: 'rounded-b-lg'} overflow-hidden"
|
||||||
|
>
|
||||||
|
<CodeEditor
|
||||||
|
value={code}
|
||||||
|
{id}
|
||||||
|
{lang}
|
||||||
|
on:save={() => {
|
||||||
|
saveCode();
|
||||||
|
}}
|
||||||
|
on:change={(e) => {
|
||||||
|
_code = e.detail.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <pre
|
||||||
class=" hljs p-4 px-5 overflow-x-auto"
|
class=" hljs p-4 px-5 overflow-x-auto"
|
||||||
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
|
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
|
||||||
stdout ||
|
stdout ||
|
||||||
stderr ||
|
stderr ||
|
||||||
result) &&
|
result) &&
|
||||||
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
|
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code></code></pre> -->
|
||||||
class="language-{lang} rounded-t-none whitespace-pre"
|
|
||||||
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
|
|
||||||
></pre>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="plt-canvas-{id}"
|
id="plt-canvas-{id}"
|
||||||
|
@ -56,7 +56,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={contentContainerElement}>
|
<div bind:this={contentContainerElement}>
|
||||||
<Markdown {id} {content} {model} />
|
<Markdown
|
||||||
|
{id}
|
||||||
|
{content}
|
||||||
|
{model}
|
||||||
|
on:update={(e) => {
|
||||||
|
dispatch('update', e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if floatingButtons}
|
{#if floatingButtons}
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
import markedKatexExtension from '$lib/utils/marked/katex-extension';
|
import markedKatexExtension from '$lib/utils/marked/katex-extension';
|
||||||
|
|
||||||
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
|
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let id;
|
export let id;
|
||||||
export let content;
|
export let content;
|
||||||
@ -31,5 +34,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key id}
|
{#key id}
|
||||||
<MarkdownTokens {tokens} {id} />
|
<MarkdownTokens
|
||||||
|
{tokens}
|
||||||
|
{id}
|
||||||
|
on:update={(e) => {
|
||||||
|
dispatch('update', e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { marked, type Token } from 'marked';
|
import { marked, type Token } from 'marked';
|
||||||
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
|
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
|
||||||
|
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
||||||
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
|
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
|
||||||
import KatexRenderer from './KatexRenderer.svelte';
|
import KatexRenderer from './KatexRenderer.svelte';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
|
||||||
import { stringify } from 'postcss';
|
|
||||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let tokens: Token[];
|
export let tokens: Token[];
|
||||||
export let top = true;
|
export let top = true;
|
||||||
@ -34,6 +36,12 @@
|
|||||||
{token}
|
{token}
|
||||||
lang={token?.lang ?? ''}
|
lang={token?.lang ?? ''}
|
||||||
code={revertSanitizedResponseContent(token?.text ?? '')}
|
code={revertSanitizedResponseContent(token?.text ?? '')}
|
||||||
|
on:save={(e) => {
|
||||||
|
dispatch('update', {
|
||||||
|
oldContent: token.text,
|
||||||
|
newContent: e.detail
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if token.type === 'table'}
|
{:else if token.type === 'table'}
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||||
|
@ -479,6 +479,15 @@
|
|||||||
id={message.id}
|
id={message.id}
|
||||||
content={message.content}
|
content={message.content}
|
||||||
{model}
|
{model}
|
||||||
|
on:update={(e) => {
|
||||||
|
const { oldContent, newContent } = e.detail;
|
||||||
|
|
||||||
|
history.messages[message.id].content = history.messages[
|
||||||
|
message.id
|
||||||
|
].content.replace(oldContent, newContent);
|
||||||
|
|
||||||
|
dispatch('update');
|
||||||
|
}}
|
||||||
on:explain={(e) => {
|
on:explain={(e) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
'submit',
|
'submit',
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
import { indentUnit } from '@codemirror/language';
|
import { indentUnit } from '@codemirror/language';
|
||||||
import { python } from '@codemirror/lang-python';
|
import { python } from '@codemirror/lang-python';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
|
||||||
import { onMount, createEventDispatcher, getContext } from 'svelte';
|
import { onMount, createEventDispatcher, getContext } from 'svelte';
|
||||||
@ -19,15 +21,41 @@
|
|||||||
|
|
||||||
export let boilerplate = '';
|
export let boilerplate = '';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
|
let _value = '';
|
||||||
|
|
||||||
|
$: if (value) {
|
||||||
|
updateValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValue = () => {
|
||||||
|
_value = value;
|
||||||
|
if (codeEditor) {
|
||||||
|
codeEditor.dispatch({
|
||||||
|
changes: [{ from: 0, to: codeEditor.state.doc.length, insert: _value }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export let id = '';
|
||||||
|
export let lang = '';
|
||||||
|
|
||||||
let codeEditor;
|
let codeEditor;
|
||||||
|
|
||||||
let isDarkMode = false;
|
let isDarkMode = false;
|
||||||
let editorTheme = new Compartment();
|
let editorTheme = new Compartment();
|
||||||
|
|
||||||
|
const getLang = () => {
|
||||||
|
if (lang === 'python') {
|
||||||
|
return python();
|
||||||
|
} else if (lang === 'javascript') {
|
||||||
|
return javascript();
|
||||||
|
}
|
||||||
|
return python();
|
||||||
|
};
|
||||||
|
|
||||||
export const formatPythonCodeHandler = async () => {
|
export const formatPythonCodeHandler = async () => {
|
||||||
if (codeEditor) {
|
if (codeEditor) {
|
||||||
const res = await formatPythonCode(value).catch((error) => {
|
const res = await formatPythonCode(_value).catch((error) => {
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@ -49,12 +77,13 @@
|
|||||||
let extensions = [
|
let extensions = [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]),
|
keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]),
|
||||||
python(),
|
getLang(),
|
||||||
indentUnit.of(' '),
|
indentUnit.of(' '),
|
||||||
placeholder('Enter your code here...'),
|
placeholder('Enter your code here...'),
|
||||||
EditorView.updateListener.of((e) => {
|
EditorView.updateListener.of((e) => {
|
||||||
if (e.docChanged) {
|
if (e.docChanged) {
|
||||||
value = e.state.doc.toString();
|
_value = e.state.doc.toString();
|
||||||
|
dispatch('change', { value: _value });
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
editorTheme.of([])
|
editorTheme.of([])
|
||||||
@ -66,16 +95,18 @@
|
|||||||
value = boilerplate;
|
value = boilerplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_value = value;
|
||||||
|
|
||||||
// Check if html class has dark mode
|
// Check if html class has dark mode
|
||||||
isDarkMode = document.documentElement.classList.contains('dark');
|
isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
// python code editor, highlight python code
|
// python code editor, highlight python code
|
||||||
codeEditor = new EditorView({
|
codeEditor = new EditorView({
|
||||||
state: EditorState.create({
|
state: EditorState.create({
|
||||||
doc: value,
|
doc: _value,
|
||||||
extensions: extensions
|
extensions: extensions
|
||||||
}),
|
}),
|
||||||
parent: document.getElementById('code-textarea')
|
parent: document.getElementById(`code-textarea-${id}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDarkMode) {
|
if (isDarkMode) {
|
||||||
@ -133,4 +164,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="code-textarea" class="h-full w-full" />
|
<div id="code-textarea-{id}" class="h-full w-full" />
|
||||||
|
@ -21,6 +21,15 @@
|
|||||||
description: ''
|
description: ''
|
||||||
};
|
};
|
||||||
export let content = '';
|
export let content = '';
|
||||||
|
let _content = '';
|
||||||
|
|
||||||
|
$: if (content) {
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateContent = () => {
|
||||||
|
_content = content;
|
||||||
|
};
|
||||||
|
|
||||||
$: if (name && !edit && !clone) {
|
$: if (name && !edit && !clone) {
|
||||||
id = name.replace(/\s+/g, '_').toLowerCase();
|
id = name.replace(/\s+/g, '_').toLowerCase();
|
||||||
@ -336,10 +345,14 @@ class Pipe:
|
|||||||
|
|
||||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
bind:value={content}
|
|
||||||
bind:this={codeEditor}
|
bind:this={codeEditor}
|
||||||
|
value={content}
|
||||||
{boilerplate}
|
{boilerplate}
|
||||||
|
on:change={(e) => {
|
||||||
|
_content = e.detail.value;
|
||||||
|
}}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
|
content = _content;
|
||||||
if (formElement) {
|
if (formElement) {
|
||||||
formElement.requestSubmit();
|
formElement.requestSubmit();
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,15 @@
|
|||||||
description: ''
|
description: ''
|
||||||
};
|
};
|
||||||
export let content = '';
|
export let content = '';
|
||||||
|
let _content = '';
|
||||||
|
|
||||||
|
$: if (content) {
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateContent = () => {
|
||||||
|
_content = content;
|
||||||
|
};
|
||||||
|
|
||||||
$: if (name && !edit && !clone) {
|
$: if (name && !edit && !clone) {
|
||||||
id = name.replace(/\s+/g, '_').toLowerCase();
|
id = name.replace(/\s+/g, '_').toLowerCase();
|
||||||
@ -224,10 +233,14 @@ class Tools:
|
|||||||
|
|
||||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
bind:value={content}
|
|
||||||
bind:this={codeEditor}
|
bind:this={codeEditor}
|
||||||
|
value={content}
|
||||||
{boilerplate}
|
{boilerplate}
|
||||||
|
on:change={(e) => {
|
||||||
|
_content = e.detail.value;
|
||||||
|
}}
|
||||||
on:save={() => {
|
on:save={() => {
|
||||||
|
content = _content;
|
||||||
if (formElement) {
|
if (formElement) {
|
||||||
formElement.requestSubmit();
|
formElement.requestSubmit();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user