mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
feat/enh: insert prompt as rich text
This commit is contained in:
parent
ade4b0d691
commit
5722da8e3b
3 changed files with 107 additions and 38 deletions
|
|
@ -1182,6 +1182,7 @@
|
||||||
}}
|
}}
|
||||||
json={true}
|
json={true}
|
||||||
messageInput={true}
|
messageInput={true}
|
||||||
|
insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false}
|
||||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||||
(!$mobile ||
|
(!$mobile ||
|
||||||
!(
|
!(
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
let detectArtifacts = true;
|
let detectArtifacts = true;
|
||||||
|
|
||||||
let richTextInput = true;
|
let richTextInput = true;
|
||||||
|
let insertPromptAsRichText = false;
|
||||||
let promptAutocomplete = false;
|
let promptAutocomplete = false;
|
||||||
|
|
||||||
let largeTextAsFile = false;
|
let largeTextAsFile = false;
|
||||||
|
|
@ -218,6 +219,11 @@
|
||||||
saveSettings({ richTextInput });
|
saveSettings({ richTextInput });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleInsertPromptAsRichText = async () => {
|
||||||
|
insertPromptAsRichText = !insertPromptAsRichText;
|
||||||
|
saveSettings({ insertPromptAsRichText });
|
||||||
|
};
|
||||||
|
|
||||||
const toggleLargeTextAsFile = async () => {
|
const toggleLargeTextAsFile = async () => {
|
||||||
largeTextAsFile = !largeTextAsFile;
|
largeTextAsFile = !largeTextAsFile;
|
||||||
saveSettings({ largeTextAsFile });
|
saveSettings({ largeTextAsFile });
|
||||||
|
|
@ -308,7 +314,9 @@
|
||||||
voiceInterruption = $settings?.voiceInterruption ?? false;
|
voiceInterruption = $settings?.voiceInterruption ?? false;
|
||||||
|
|
||||||
richTextInput = $settings?.richTextInput ?? true;
|
richTextInput = $settings?.richTextInput ?? true;
|
||||||
|
insertPromptAsRichText = $settings?.insertPromptAsRichText ?? false;
|
||||||
promptAutocomplete = $settings?.promptAutocomplete ?? false;
|
promptAutocomplete = $settings?.promptAutocomplete ?? false;
|
||||||
|
|
||||||
largeTextAsFile = $settings?.largeTextAsFile ?? false;
|
largeTextAsFile = $settings?.largeTextAsFile ?? false;
|
||||||
copyFormatted = $settings?.copyFormatted ?? false;
|
copyFormatted = $settings?.copyFormatted ?? false;
|
||||||
|
|
||||||
|
|
@ -761,7 +769,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config?.features?.enable_autocomplete_generation && richTextInput}
|
{#if richTextInput}
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div id="rich-input-label" class=" self-center text-xs">
|
||||||
|
{$i18n.t('Insert Prompt as Rich Text')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
aria-labelledby="rich-input-label"
|
||||||
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||||
|
on:click={() => {
|
||||||
|
toggleInsertPromptAsRichText();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if insertPromptAsRichText === true}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_autocomplete_generation}
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div id="prompt-autocompletion-label" class=" self-center text-xs">
|
<div id="prompt-autocompletion-label" class=" self-center text-xs">
|
||||||
|
|
@ -785,6 +817,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
import TurndownService from 'turndown';
|
import TurndownService from 'turndown';
|
||||||
import { gfm } from 'turndown-plugin-gfm';
|
import { gfm } from 'turndown-plugin-gfm';
|
||||||
const turndownService = new TurndownService({
|
const turndownService = new TurndownService({
|
||||||
|
|
@ -7,7 +8,6 @@
|
||||||
headingStyle: 'atx'
|
headingStyle: 'atx'
|
||||||
});
|
});
|
||||||
turndownService.escape = (string) => string;
|
turndownService.escape = (string) => string;
|
||||||
|
|
||||||
// Use turndown-plugin-gfm for proper GFM table support
|
// Use turndown-plugin-gfm for proper GFM table support
|
||||||
turndownService.use(gfm);
|
turndownService.use(gfm);
|
||||||
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
|
|
||||||
const eventDispatch = createEventDispatcher();
|
const eventDispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { Fragment } from 'prosemirror-model';
|
import { Fragment, DOMParser } from 'prosemirror-model';
|
||||||
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
|
||||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
|
|
||||||
|
|
@ -29,10 +29,10 @@
|
||||||
|
|
||||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import { all, createLowlight } from 'lowlight';
|
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import Highlight from '@tiptap/extension-highlight';
|
import Highlight from '@tiptap/extension-highlight';
|
||||||
import Typography from '@tiptap/extension-typography';
|
import Typography from '@tiptap/extension-typography';
|
||||||
|
import { all, createLowlight } from 'lowlight';
|
||||||
|
|
||||||
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
||||||
|
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
export let messageInput = false;
|
export let messageInput = false;
|
||||||
export let shiftEnter = false;
|
export let shiftEnter = false;
|
||||||
export let largeTextAsFile = false;
|
export let largeTextAsFile = false;
|
||||||
|
export let insertPromptAsRichText = false;
|
||||||
|
|
||||||
let element;
|
let element;
|
||||||
let editor;
|
let editor;
|
||||||
|
|
@ -130,6 +131,39 @@
|
||||||
|
|
||||||
let tr = state.tr;
|
let tr = state.tr;
|
||||||
|
|
||||||
|
if (insertPromptAsRichText) {
|
||||||
|
const htmlContent = marked
|
||||||
|
.parse(text, {
|
||||||
|
breaks: true,
|
||||||
|
gfm: true
|
||||||
|
})
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Create a temporary div to parse HTML
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = htmlContent;
|
||||||
|
|
||||||
|
// Convert HTML to ProseMirror nodes
|
||||||
|
const fragment = DOMParser.fromSchema(state.schema).parse(tempDiv);
|
||||||
|
|
||||||
|
// Extract just the content, not the wrapper paragraphs
|
||||||
|
const content = fragment.content;
|
||||||
|
let nodesToInsert = [];
|
||||||
|
|
||||||
|
content.forEach((node) => {
|
||||||
|
if (node.type.name === 'paragraph') {
|
||||||
|
// If it's a paragraph, extract its content
|
||||||
|
nodesToInsert.push(...node.content.content);
|
||||||
|
} else {
|
||||||
|
nodesToInsert.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tr = tr.replaceWith(start, end, nodesToInsert);
|
||||||
|
// Calculate new position
|
||||||
|
const newPos = start + nodesToInsert.reduce((sum, node) => sum + node.nodeSize, 0);
|
||||||
|
tr = tr.setSelection(Selection.near(tr.doc.resolve(newPos)));
|
||||||
|
} else {
|
||||||
if (text.includes('\n')) {
|
if (text.includes('\n')) {
|
||||||
// Split the text into lines and create a <p> node for each line
|
// Split the text into lines and create a <p> node for each line
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
|
|
@ -165,6 +199,7 @@
|
||||||
state.selection.constructor.near(tr.doc.resolve(start + text.length + 1))
|
state.selection.constructor.near(tr.doc.resolve(start + text.length + 1))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(tr);
|
dispatch(tr);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue