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}
|
||||
messageInput={true}
|
||||
insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false}
|
||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||
(!$mobile ||
|
||||
!(
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
let detectArtifacts = true;
|
||||
|
||||
let richTextInput = true;
|
||||
let insertPromptAsRichText = false;
|
||||
let promptAutocomplete = false;
|
||||
|
||||
let largeTextAsFile = false;
|
||||
|
|
@ -218,6 +219,11 @@
|
|||
saveSettings({ richTextInput });
|
||||
};
|
||||
|
||||
const toggleInsertPromptAsRichText = async () => {
|
||||
insertPromptAsRichText = !insertPromptAsRichText;
|
||||
saveSettings({ insertPromptAsRichText });
|
||||
};
|
||||
|
||||
const toggleLargeTextAsFile = async () => {
|
||||
largeTextAsFile = !largeTextAsFile;
|
||||
saveSettings({ largeTextAsFile });
|
||||
|
|
@ -308,7 +314,9 @@
|
|||
voiceInterruption = $settings?.voiceInterruption ?? false;
|
||||
|
||||
richTextInput = $settings?.richTextInput ?? true;
|
||||
insertPromptAsRichText = $settings?.insertPromptAsRichText ?? false;
|
||||
promptAutocomplete = $settings?.promptAutocomplete ?? false;
|
||||
|
||||
largeTextAsFile = $settings?.largeTextAsFile ?? false;
|
||||
copyFormatted = $settings?.copyFormatted ?? false;
|
||||
|
||||
|
|
@ -761,7 +769,31 @@
|
|||
</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 class=" py-0.5 flex w-full justify-between">
|
||||
<div id="prompt-autocompletion-label" class=" self-center text-xs">
|
||||
|
|
@ -785,6 +817,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { marked } from 'marked';
|
||||
|
||||
import TurndownService from 'turndown';
|
||||
import { gfm } from 'turndown-plugin-gfm';
|
||||
const turndownService = new TurndownService({
|
||||
|
|
@ -7,7 +8,6 @@
|
|||
headingStyle: 'atx'
|
||||
});
|
||||
turndownService.escape = (string) => string;
|
||||
|
||||
// Use turndown-plugin-gfm for proper GFM table support
|
||||
turndownService.use(gfm);
|
||||
|
||||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
const eventDispatch = createEventDispatcher();
|
||||
|
||||
import { Fragment } from 'prosemirror-model';
|
||||
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
||||
import { Fragment, DOMParser } from 'prosemirror-model';
|
||||
import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { Editor } from '@tiptap/core';
|
||||
|
||||
|
|
@ -29,10 +29,10 @@
|
|||
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import Typography from '@tiptap/extension-typography';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
|
||||
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
||||
|
||||
|
|
@ -60,6 +60,7 @@
|
|||
export let messageInput = false;
|
||||
export let shiftEnter = false;
|
||||
export let largeTextAsFile = false;
|
||||
export let insertPromptAsRichText = false;
|
||||
|
||||
let element;
|
||||
let editor;
|
||||
|
|
@ -130,6 +131,39 @@
|
|||
|
||||
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')) {
|
||||
// Split the text into lines and create a <p> node for each line
|
||||
const lines = text.split('\n');
|
||||
|
|
@ -165,6 +199,7 @@
|
|||
state.selection.constructor.near(tr.doc.resolve(start + text.length + 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(tr);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue