feat: editable code block

This commit is contained in:
Timothy J. Baek 2024-10-05 12:04:36 -07:00
parent 82ac2e4edb
commit 81440460f2
8 changed files with 155 additions and 31 deletions

View File

@ -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}"

View File

@ -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}

View File

@ -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}

View File

@ -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">

View File

@ -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',

View File

@ -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" />

View File

@ -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();
} }

View File

@ -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();
} }