refac: deprecate textarea input

This commit is contained in:
Timothy Jaeryang Baek 2025-09-12 20:54:34 +04:00
parent 19e18bc461
commit 153afd832c
3 changed files with 60 additions and 303 deletions

View file

@ -1464,18 +1464,8 @@
prompt = '';
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));
chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
chatFiles = chatFiles.filter(
// Remove duplicates
@ -2378,11 +2368,7 @@
if (e.detail || files.length > 0) {
await tick();
submitPrompt(
($settings?.richTextInput ?? true)
? e.detail.replaceAll('\n\n', '\n')
: e.detail
);
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
}
}}
/>
@ -2431,11 +2417,7 @@
clearDraft();
if (e.detail || files.length > 0) {
await tick();
submitPrompt(
($settings?.richTextInput ?? true)
? e.detail.replaceAll('\n\n', '\n')
: e.detail
);
submitPrompt(e.detail.replaceAll('\n\n', '\n'));
}
}}
/>

View file

@ -277,29 +277,8 @@
const chatInput = document.getElementById('chat-input');
if (chatInput) {
if ($settings?.richTextInput ?? true) {
chatInputElement.replaceVariables(variables);
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 }));
}
chatInputElement.replaceVariables(variables);
chatInputElement.focus();
}
};
@ -309,16 +288,8 @@
if (chatInput) {
text = await textVariableHandler(text || '');
if ($settings?.richTextInput ?? true) {
chatInputElement?.setText(text);
chatInputElement?.focus();
} else {
chatInput.value = text;
prompt = text;
chatInput.focus();
chatInput.dispatchEvent(new Event('input'));
}
chatInputElement?.setText(text);
chatInputElement?.focus();
text = await inputVariableHandler(text);
await tick();
@ -341,12 +312,7 @@
let word = '';
if (chatInput) {
if ($settings?.richTextInput ?? true) {
word = chatInputElement?.getWordAtDocPos();
} else {
const cursor = chatInput ? chatInput.selectionStart : prompt.length;
word = getWordAtCursor(prompt, cursor);
}
word = chatInputElement?.getWordAtDocPos();
}
return word;
@ -364,15 +330,7 @@
const chatInput = document.getElementById('chat-input');
if (!chatInput) return;
if ($settings?.richTextInput ?? true) {
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);
}
chatInputElement?.replaceCommandWithText(text);
};
const insertTextAtCursor = async (text: string) => {
@ -384,14 +342,7 @@
if (command) {
replaceCommandWithText(text);
} else {
if ($settings?.richTextInput ?? true) {
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);
}
chatInputElement?.insertContent(text);
}
await tick();
@ -413,18 +364,6 @@
if (words.length > 0) {
const word = words.at(0);
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 {
chatInput.scrollTop = chatInput.scrollHeight;
}
@ -1230,12 +1169,12 @@
{/if}
<div class="px-2.5">
{#if $settings?.richTextInput ?? true}
<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"
id="chat-input-container"
>
{#if suggestions}
<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"
id="chat-input-container"
>
{#if suggestions}
{#key $settings?.richTextInput ?? true}
{#key $settings?.showFormattingToolbar ?? false}
<RichTextInput
bind:this={chatInputElement}
@ -1245,6 +1184,7 @@
command = getCommand();
}}
json={true}
richText={$settings?.richTextInput ?? true}
messageInput={true}
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
floatingMenuPlacement={'top-start'}
@ -1429,195 +1369,9 @@
}}
/>
{/key}
{/if}
</div>
{:else}
<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}
{/key}
{/if}
</div>
</div>
<div class=" flex justify-between mt-0.5 mb-2.5 mx-0.5 max-w-full" dir="ltr">

View file

@ -162,10 +162,11 @@
export let className = 'input-prose';
export let placeholder = 'Type here...';
export let richText = true;
export let link = false;
export let image = false;
export let fileHandler = false;
export let suggestions = null;
export let onFileDrop = (currentEditor, files, pos) => {
@ -964,11 +965,23 @@
Placeholder.configure({ placeholder }),
SelectionDecoration,
CodeBlockLowlight.configure({
lowlight
}),
Highlight,
Typography,
...(richText
? [
CodeBlockLowlight.configure({
lowlight
}),
Highlight,
Typography,
TableKit.configure({
table: { resizable: true }
}),
ListKit.configure({
taskItem: {
nested: true
}
})
]
: []),
...(suggestions
? [
Mention.configure({
@ -978,14 +991,6 @@
]
: []),
TableKit.configure({
table: { resizable: true }
}),
ListKit.configure({
taskItem: {
nested: true
}
}),
CharacterCount.configure({}),
...(image ? [Image] : []),
...(fileHandler
@ -996,8 +1001,7 @@
})
]
: []),
...(autocomplete
...(richText && autocomplete
? [
AIAutocompletion.configure({
generateCompletion: async (text) => {
@ -1015,8 +1019,7 @@
})
]
: []),
...(showFormattingToolbar
...(richText && showFormattingToolbar
? [
BubbleMenu.configure({
element: bubbleMenuElement,
@ -1091,6 +1094,22 @@
},
editorProps: {
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: {
compositionstart: (view, event) => {
oncompositionstart(event);
@ -1277,7 +1296,9 @@
editor.storage.files = files;
}
},
onSelectionUpdate: onSelectionUpdate
onSelectionUpdate: onSelectionUpdate,
enableInputRules: richText,
enablePasteRules: richText
});
if (messageInput) {