mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
refac: deprecate textarea input
This commit is contained in:
parent
19e18bc461
commit
153afd832c
3 changed files with 60 additions and 303 deletions
|
|
@ -1464,18 +1464,8 @@
|
||||||
prompt = '';
|
prompt = '';
|
||||||
|
|
||||||
const messages = createMessagesList(history, history.currentId);
|
const messages = createMessagesList(history, history.currentId);
|
||||||
|
|
||||||
// Reset chat input textarea
|
|
||||||
if (!($settings?.richTextInput ?? true)) {
|
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
|
||||||
|
|
||||||
if (chatInputElement) {
|
|
||||||
await tick();
|
|
||||||
chatInputElement.style.height = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _files = JSON.parse(JSON.stringify(files));
|
const _files = JSON.parse(JSON.stringify(files));
|
||||||
|
|
||||||
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
|
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
|
||||||
chatFiles = chatFiles.filter(
|
chatFiles = chatFiles.filter(
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
|
|
@ -2378,11 +2368,7 @@
|
||||||
if (e.detail || files.length > 0) {
|
if (e.detail || files.length > 0) {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
submitPrompt(
|
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
|
||||||
($settings?.richTextInput ?? true)
|
|
||||||
? e.detail.replaceAll('\n\n', '\n')
|
|
||||||
: e.detail
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -2431,11 +2417,7 @@
|
||||||
clearDraft();
|
clearDraft();
|
||||||
if (e.detail || files.length > 0) {
|
if (e.detail || files.length > 0) {
|
||||||
await tick();
|
await tick();
|
||||||
submitPrompt(
|
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
|
||||||
($settings?.richTextInput ?? true)
|
|
||||||
? e.detail.replaceAll('\n\n', '\n')
|
|
||||||
: e.detail
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -277,29 +277,8 @@
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
if (chatInput) {
|
if (chatInput) {
|
||||||
if ($settings?.richTextInput ?? true) {
|
chatInputElement.replaceVariables(variables);
|
||||||
chatInputElement.replaceVariables(variables);
|
chatInputElement.focus();
|
||||||
chatInputElement.focus();
|
|
||||||
} else {
|
|
||||||
// Get current value from the input element
|
|
||||||
let currentValue = chatInput.value || '';
|
|
||||||
|
|
||||||
// Replace template variables using regex
|
|
||||||
const updatedValue = currentValue.replace(
|
|
||||||
/{{\s*([^|}]+)(?:\|[^}]*)?\s*}}/g,
|
|
||||||
(match, varName) => {
|
|
||||||
const trimmedVarName = varName.trim();
|
|
||||||
return variables.hasOwnProperty(trimmedVarName)
|
|
||||||
? String(variables[trimmedVarName])
|
|
||||||
: match;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the input value
|
|
||||||
chatInput.value = updatedValue;
|
|
||||||
chatInput.focus();
|
|
||||||
chatInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -309,16 +288,8 @@
|
||||||
if (chatInput) {
|
if (chatInput) {
|
||||||
text = await textVariableHandler(text || '');
|
text = await textVariableHandler(text || '');
|
||||||
|
|
||||||
if ($settings?.richTextInput ?? true) {
|
chatInputElement?.setText(text);
|
||||||
chatInputElement?.setText(text);
|
chatInputElement?.focus();
|
||||||
chatInputElement?.focus();
|
|
||||||
} else {
|
|
||||||
chatInput.value = text;
|
|
||||||
prompt = text;
|
|
||||||
|
|
||||||
chatInput.focus();
|
|
||||||
chatInput.dispatchEvent(new Event('input'));
|
|
||||||
}
|
|
||||||
|
|
||||||
text = await inputVariableHandler(text);
|
text = await inputVariableHandler(text);
|
||||||
await tick();
|
await tick();
|
||||||
|
|
@ -341,12 +312,7 @@
|
||||||
let word = '';
|
let word = '';
|
||||||
|
|
||||||
if (chatInput) {
|
if (chatInput) {
|
||||||
if ($settings?.richTextInput ?? true) {
|
word = chatInputElement?.getWordAtDocPos();
|
||||||
word = chatInputElement?.getWordAtDocPos();
|
|
||||||
} else {
|
|
||||||
const cursor = chatInput ? chatInput.selectionStart : prompt.length;
|
|
||||||
word = getWordAtCursor(prompt, cursor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return word;
|
return word;
|
||||||
|
|
@ -364,15 +330,7 @@
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
if (!chatInput) return;
|
if (!chatInput) return;
|
||||||
|
|
||||||
if ($settings?.richTextInput ?? true) {
|
chatInputElement?.replaceCommandWithText(text);
|
||||||
chatInputElement?.replaceCommandWithText(text);
|
|
||||||
} else {
|
|
||||||
const cursor = chatInput.selectionStart;
|
|
||||||
const { start, end } = getWordBoundsAtCursor(prompt, cursor);
|
|
||||||
prompt = prompt.slice(0, start) + text + prompt.slice(end);
|
|
||||||
chatInput.focus();
|
|
||||||
chatInput.setSelectionRange(start + text.length, start + text.length);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const insertTextAtCursor = async (text: string) => {
|
const insertTextAtCursor = async (text: string) => {
|
||||||
|
|
@ -384,14 +342,7 @@
|
||||||
if (command) {
|
if (command) {
|
||||||
replaceCommandWithText(text);
|
replaceCommandWithText(text);
|
||||||
} else {
|
} else {
|
||||||
if ($settings?.richTextInput ?? true) {
|
chatInputElement?.insertContent(text);
|
||||||
chatInputElement?.insertContent(text);
|
|
||||||
} else {
|
|
||||||
const cursor = chatInput.selectionStart;
|
|
||||||
prompt = prompt.slice(0, cursor) + text + prompt.slice(cursor);
|
|
||||||
chatInput.focus();
|
|
||||||
chatInput.setSelectionRange(cursor + text.length, cursor + text.length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
@ -413,18 +364,6 @@
|
||||||
if (words.length > 0) {
|
if (words.length > 0) {
|
||||||
const word = words.at(0);
|
const word = words.at(0);
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
if (!($settings?.richTextInput ?? true)) {
|
|
||||||
// Move scroll to the first word
|
|
||||||
chatInput.setSelectionRange(word.startIndex, word.endIndex + 1);
|
|
||||||
chatInput.focus();
|
|
||||||
|
|
||||||
const selectionRow =
|
|
||||||
(word?.startIndex - (word?.startIndex % chatInput.cols)) / chatInput.cols;
|
|
||||||
const lineHeight = chatInput.clientHeight / chatInput.rows;
|
|
||||||
|
|
||||||
chatInput.scrollTop = lineHeight * selectionRow;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
chatInput.scrollTop = chatInput.scrollHeight;
|
chatInput.scrollTop = chatInput.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
@ -1230,12 +1169,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="px-2.5">
|
<div class="px-2.5">
|
||||||
{#if $settings?.richTextInput ?? true}
|
<div
|
||||||
<div
|
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-2.5 pb-[5px] px-1 resize-none h-fit max-h-80 overflow-auto"
|
||||||
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-2.5 pb-[5px] px-1 resize-none h-fit max-h-80 overflow-auto"
|
id="chat-input-container"
|
||||||
id="chat-input-container"
|
>
|
||||||
>
|
{#if suggestions}
|
||||||
{#if suggestions}
|
{#key $settings?.richTextInput ?? true}
|
||||||
{#key $settings?.showFormattingToolbar ?? false}
|
{#key $settings?.showFormattingToolbar ?? false}
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
bind:this={chatInputElement}
|
bind:this={chatInputElement}
|
||||||
|
|
@ -1245,6 +1184,7 @@
|
||||||
command = getCommand();
|
command = getCommand();
|
||||||
}}
|
}}
|
||||||
json={true}
|
json={true}
|
||||||
|
richText={$settings?.richTextInput ?? true}
|
||||||
messageInput={true}
|
messageInput={true}
|
||||||
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
|
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
|
||||||
floatingMenuPlacement={'top-start'}
|
floatingMenuPlacement={'top-start'}
|
||||||
|
|
@ -1429,195 +1369,9 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/key}
|
||||||
</div>
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<textarea
|
|
||||||
id="chat-input"
|
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
|
||||||
bind:this={chatInputElement}
|
|
||||||
class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-4 pb-1 px-1 resize-none"
|
|
||||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
|
||||||
bind:value={prompt}
|
|
||||||
on:input={() => {
|
|
||||||
command = getCommand();
|
|
||||||
}}
|
|
||||||
on:click={() => {
|
|
||||||
command = getCommand();
|
|
||||||
}}
|
|
||||||
on:compositionstart={() => (isComposing = true)}
|
|
||||||
on:compositionend={(e) => {
|
|
||||||
compositionEndedAt = e.timeStamp;
|
|
||||||
isComposing = false;
|
|
||||||
}}
|
|
||||||
on:keydown={async (e) => {
|
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
|
||||||
|
|
||||||
const suggestionsContainerElement =
|
|
||||||
document.getElementById('suggestions-container');
|
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
stopResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
|
||||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
createMessagePair(prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Ctrl + R is pressed
|
|
||||||
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log('regenerate');
|
|
||||||
|
|
||||||
const regenerateButton = [
|
|
||||||
...document.getElementsByClassName('regenerate-response-button')
|
|
||||||
]?.at(-1);
|
|
||||||
|
|
||||||
regenerateButton?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prompt === '' && e.key == 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const userMessageElement = [
|
|
||||||
...document.getElementsByClassName('user-message')
|
|
||||||
]?.at(-1);
|
|
||||||
|
|
||||||
const editButton = [
|
|
||||||
...document.getElementsByClassName('edit-user-message-button')
|
|
||||||
]?.at(-1);
|
|
||||||
|
|
||||||
console.log(userMessageElement);
|
|
||||||
|
|
||||||
userMessageElement?.scrollIntoView({ block: 'center' });
|
|
||||||
editButton?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!suggestionsContainerElement) {
|
|
||||||
if (
|
|
||||||
!$mobile ||
|
|
||||||
!(
|
|
||||||
'ontouchstart' in window ||
|
|
||||||
navigator.maxTouchPoints > 0 ||
|
|
||||||
navigator.msMaxTouchPoints > 0
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (inOrNearComposition(e)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent Enter key from creating a new line
|
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey;
|
|
||||||
const enterPressed =
|
|
||||||
($settings?.ctrlEnterToSend ?? false)
|
|
||||||
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
|
|
||||||
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
|
|
||||||
|
|
||||||
if (enterPressed) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit the prompt when Enter key is pressed
|
|
||||||
if ((prompt !== '' || files.length > 0) && enterPressed) {
|
|
||||||
dispatch('submit', prompt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
const words = extractCurlyBraceWords(prompt);
|
|
||||||
|
|
||||||
if (words.length > 0) {
|
|
||||||
const word = words.at(0);
|
|
||||||
|
|
||||||
if (word && e.target instanceof HTMLTextAreaElement) {
|
|
||||||
// Prevent default tab behavior
|
|
||||||
e.preventDefault();
|
|
||||||
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
|
||||||
e.target.focus();
|
|
||||||
|
|
||||||
const selectionRow =
|
|
||||||
(word?.startIndex - (word?.startIndex % e.target.cols)) /
|
|
||||||
e.target.cols;
|
|
||||||
const lineHeight = e.target.clientHeight / e.target.rows;
|
|
||||||
|
|
||||||
e.target.scrollTop = lineHeight * selectionRow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
e.target.style.height = '';
|
|
||||||
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
console.log('Escape');
|
|
||||||
atSelectedModel = undefined;
|
|
||||||
selectedToolIds = [];
|
|
||||||
selectedFilterIds = [];
|
|
||||||
webSearchEnabled = false;
|
|
||||||
imageGenerationEnabled = false;
|
|
||||||
codeInterpreterEnabled = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
rows="1"
|
|
||||||
on:input={async (e) => {
|
|
||||||
e.target.style.height = '';
|
|
||||||
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
|
||||||
}}
|
|
||||||
on:focus={async (e) => {
|
|
||||||
e.target.style.height = '';
|
|
||||||
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
|
||||||
}}
|
|
||||||
on:paste={async (e) => {
|
|
||||||
const clipboardData = e.clipboardData || window.clipboardData;
|
|
||||||
|
|
||||||
if (clipboardData && clipboardData.items) {
|
|
||||||
for (const item of clipboardData.items) {
|
|
||||||
console.log(item);
|
|
||||||
if (item.type.indexOf('image') !== -1) {
|
|
||||||
const blob = item.getAsFile();
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = function (e) {
|
|
||||||
files = [
|
|
||||||
...files,
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
url: `${e.target.result}`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
} else if (item?.kind === 'file') {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
if (file) {
|
|
||||||
const _files = [file];
|
|
||||||
await inputFilesHandler(_files);
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
} else if (item.type === 'text/plain') {
|
|
||||||
if (($settings?.largeTextAsFile ?? false) && !shiftKey) {
|
|
||||||
const text = clipboardData.getData('text/plain');
|
|
||||||
|
|
||||||
if (text.length > PASTED_TEXT_CHARACTER_LIMIT) {
|
|
||||||
e.preventDefault();
|
|
||||||
const blob = new Blob([text], { type: 'text/plain' });
|
|
||||||
const file = new File([blob], `Pasted_Text_${Date.now()}.txt`, {
|
|
||||||
type: 'text/plain'
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploadFileHandler(file, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex justify-between mt-0.5 mb-2.5 mx-0.5 max-w-full" dir="ltr">
|
<div class=" flex justify-between mt-0.5 mb-2.5 mx-0.5 max-w-full" dir="ltr">
|
||||||
|
|
|
||||||
|
|
@ -162,10 +162,11 @@
|
||||||
|
|
||||||
export let className = 'input-prose';
|
export let className = 'input-prose';
|
||||||
export let placeholder = 'Type here...';
|
export let placeholder = 'Type here...';
|
||||||
|
|
||||||
|
export let richText = true;
|
||||||
export let link = false;
|
export let link = false;
|
||||||
export let image = false;
|
export let image = false;
|
||||||
export let fileHandler = false;
|
export let fileHandler = false;
|
||||||
|
|
||||||
export let suggestions = null;
|
export let suggestions = null;
|
||||||
|
|
||||||
export let onFileDrop = (currentEditor, files, pos) => {
|
export let onFileDrop = (currentEditor, files, pos) => {
|
||||||
|
|
@ -964,11 +965,23 @@
|
||||||
Placeholder.configure({ placeholder }),
|
Placeholder.configure({ placeholder }),
|
||||||
SelectionDecoration,
|
SelectionDecoration,
|
||||||
|
|
||||||
CodeBlockLowlight.configure({
|
...(richText
|
||||||
lowlight
|
? [
|
||||||
}),
|
CodeBlockLowlight.configure({
|
||||||
Highlight,
|
lowlight
|
||||||
Typography,
|
}),
|
||||||
|
Highlight,
|
||||||
|
Typography,
|
||||||
|
TableKit.configure({
|
||||||
|
table: { resizable: true }
|
||||||
|
}),
|
||||||
|
ListKit.configure({
|
||||||
|
taskItem: {
|
||||||
|
nested: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(suggestions
|
...(suggestions
|
||||||
? [
|
? [
|
||||||
Mention.configure({
|
Mention.configure({
|
||||||
|
|
@ -978,14 +991,6 @@
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
||||||
TableKit.configure({
|
|
||||||
table: { resizable: true }
|
|
||||||
}),
|
|
||||||
ListKit.configure({
|
|
||||||
taskItem: {
|
|
||||||
nested: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
CharacterCount.configure({}),
|
CharacterCount.configure({}),
|
||||||
...(image ? [Image] : []),
|
...(image ? [Image] : []),
|
||||||
...(fileHandler
|
...(fileHandler
|
||||||
|
|
@ -996,8 +1001,7 @@
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(richText && autocomplete
|
||||||
...(autocomplete
|
|
||||||
? [
|
? [
|
||||||
AIAutocompletion.configure({
|
AIAutocompletion.configure({
|
||||||
generateCompletion: async (text) => {
|
generateCompletion: async (text) => {
|
||||||
|
|
@ -1015,8 +1019,7 @@
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(richText && showFormattingToolbar
|
||||||
...(showFormattingToolbar
|
|
||||||
? [
|
? [
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
element: bubbleMenuElement,
|
element: bubbleMenuElement,
|
||||||
|
|
@ -1091,6 +1094,22 @@
|
||||||
},
|
},
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: { id },
|
attributes: { id },
|
||||||
|
handlePaste: (view, event) => {
|
||||||
|
// Force plain-text pasting when richText === false
|
||||||
|
if (!richText) {
|
||||||
|
const text = (event.clipboardData?.getData('text/plain') ?? '').replace(/\r\n/g, '\n');
|
||||||
|
// swallow HTML completely
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Insert as pure text (no HTML parsing)
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
const { from, to } = state.selection;
|
||||||
|
dispatch(state.tr.insertText(text, from, to).scrollIntoView());
|
||||||
|
return true; // handled
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
compositionstart: (view, event) => {
|
compositionstart: (view, event) => {
|
||||||
oncompositionstart(event);
|
oncompositionstart(event);
|
||||||
|
|
@ -1277,7 +1296,9 @@
|
||||||
editor.storage.files = files;
|
editor.storage.files = files;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSelectionUpdate: onSelectionUpdate
|
onSelectionUpdate: onSelectionUpdate,
|
||||||
|
enableInputRules: richText,
|
||||||
|
enablePasteRules: richText
|
||||||
});
|
});
|
||||||
|
|
||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue