mirror of
https://github.com/open-webui/open-webui
synced 2024-11-21 15:47:49 +00:00
feat: rich text input for chat
This commit is contained in:
parent
5e96922eba
commit
f46b95300b
@ -30,7 +30,7 @@ describe('Settings', () => {
|
||||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
@ -50,7 +50,7 @@ describe('Settings', () => {
|
||||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
@ -85,7 +85,7 @@ describe('Settings', () => {
|
||||
// Select the first model
|
||||
cy.get('button[aria-label="model-item"]').first().click();
|
||||
// Type a message
|
||||
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
|
||||
cy.get('#chat-input').type('Hi, what can you do? A single sentence only please.', {
|
||||
force: true
|
||||
});
|
||||
// Send the message
|
||||
|
@ -125,7 +125,7 @@
|
||||
loaded = true;
|
||||
|
||||
window.setTimeout(() => scrollToBottom(), 0);
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
} else {
|
||||
await goto('/');
|
||||
@ -264,7 +264,7 @@
|
||||
if (event.data.type === 'input:prompt') {
|
||||
console.debug(event.data.text);
|
||||
|
||||
const inputElement = document.getElementById('chat-textarea');
|
||||
const inputElement = document.getElementById('chat-input');
|
||||
|
||||
if (inputElement) {
|
||||
prompt = event.data.text;
|
||||
@ -327,7 +327,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
|
||||
chats.subscribe(() => {});
|
||||
@ -501,7 +501,7 @@
|
||||
settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
||||
}
|
||||
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
setTimeout(() => chatInput?.focus(), 0);
|
||||
};
|
||||
|
||||
@ -799,7 +799,7 @@
|
||||
);
|
||||
} else {
|
||||
// Reset chat input textarea
|
||||
const chatTextAreaElement = document.getElementById('chat-textarea');
|
||||
const chatTextAreaElement = document.getElementById('chat-input');
|
||||
|
||||
if (chatTextAreaElement) {
|
||||
chatTextAreaElement.value = '';
|
||||
@ -841,6 +841,11 @@
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// focus on chat input
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
|
||||
_responses = await sendPrompt(userPrompt, userMessageId, { newChat: true });
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@
|
||||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||
import Commands from './MessageInput/Commands.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import RichTextInput from '../common/RichTextInput.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@ -53,8 +54,8 @@
|
||||
let recording = false;
|
||||
|
||||
let chatTextAreaElement: HTMLTextAreaElement;
|
||||
let chatInputContainerElement;
|
||||
let filesInputElement;
|
||||
|
||||
let commandsElement;
|
||||
|
||||
let inputFiles;
|
||||
@ -213,7 +214,10 @@
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||
window.setTimeout(() => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
}, 0);
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
@ -351,7 +355,7 @@
|
||||
recording = false;
|
||||
|
||||
await tick();
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
}}
|
||||
on:confirm={async (e) => {
|
||||
const response = e.detail;
|
||||
@ -360,7 +364,7 @@
|
||||
recording = false;
|
||||
|
||||
await tick();
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
|
||||
if ($settings?.speechAutoSend ?? false) {
|
||||
dispatch('submit', prompt);
|
||||
@ -500,35 +504,56 @@
|
||||
</InputMenu>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="chat-textarea"
|
||||
bind:this={chatTextAreaElement}
|
||||
class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
|
||||
<div
|
||||
bind:this={chatInputContainerElement}
|
||||
class="scrollbar-hidden text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px] overflow-auto"
|
||||
>
|
||||
<RichTextInput
|
||||
id="chat-input"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
on:keypress={(e) => {
|
||||
if (
|
||||
!$mobile ||
|
||||
shiftEnter={!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Prevent Enter key from creating a new line
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
||||
)}
|
||||
on:enter={async (e) => {
|
||||
if (prompt !== '') {
|
||||
dispatch('submit', prompt);
|
||||
}
|
||||
}}
|
||||
on:input={async (e) => {
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}}
|
||||
on:focus={async (e) => {
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}}
|
||||
on:keypress={(e) => {
|
||||
e = e.detail.event;
|
||||
console.log(e);
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
e = e.detail.event;
|
||||
console.log(e);
|
||||
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.style.height = '';
|
||||
chatInputContainerElement.style.height =
|
||||
Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
const commandsContainerElement = document.getElementById('commands-container');
|
||||
const commandsContainerElement =
|
||||
document.getElementById('commands-container');
|
||||
|
||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
||||
@ -636,17 +661,10 @@
|
||||
atSelectedModel = undefined;
|
||||
}
|
||||
}}
|
||||
rows="1"
|
||||
on:input={async (e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
user = null;
|
||||
}}
|
||||
on:focus={async (e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
}}
|
||||
on:paste={async (e) => {
|
||||
e = e.detail.event;
|
||||
console.log(e);
|
||||
|
||||
const clipboardData = e.clipboardData || window.clipboardData;
|
||||
|
||||
if (clipboardData && clipboardData.items) {
|
||||
@ -671,6 +689,40 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- <textarea
|
||||
id="chat-input"
|
||||
bind:this={chatTextAreaElement}
|
||||
class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
on:keypress={(e) => {
|
||||
if (
|
||||
!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Prevent Enter key from creating a new line
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Submit the prompt when Enter key is pressed
|
||||
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
||||
dispatch('submit', prompt);
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
rows="1"
|
||||
|
||||
|
||||
|
||||
/> -->
|
||||
|
||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
|
||||
|
@ -28,14 +28,14 @@
|
||||
$: command = (prompt?.trim() ?? '').split(' ')?.at(-1) ?? '';
|
||||
</script>
|
||||
|
||||
{#if ['/', '#', '@'].includes(command?.charAt(0))}
|
||||
{#if ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command.slice(0, 2)}
|
||||
{#if command?.charAt(0) === '/'}
|
||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
||||
{:else if command?.charAt(0) === '#'}
|
||||
{:else if command?.charAt(0) === '#' || '\\#' === command.slice(0, 2)}
|
||||
<Knowledge
|
||||
bind:this={commandElement}
|
||||
bind:prompt
|
||||
{command}
|
||||
command={command.includes('\\#') ? command.slice(2) : command}
|
||||
on:youtube={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
|
@ -46,7 +46,7 @@
|
||||
dispatch('select', item);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
@ -57,7 +57,7 @@
|
||||
dispatch('url', url);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
@ -68,7 +68,7 @@
|
||||
dispatch('youtube', url);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
|
@ -58,7 +58,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
|
@ -110,7 +110,7 @@
|
||||
|
||||
prompt = text;
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
|
||||
|
@ -213,7 +213,7 @@
|
||||
transcription = `${transcription}${transcript}`;
|
||||
|
||||
await tick();
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
|
||||
// Restart the inactivity timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
|
@ -330,7 +330,7 @@
|
||||
|
||||
await tick();
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
if (chatInputElement) {
|
||||
prompt = p;
|
||||
|
||||
|
@ -57,7 +57,7 @@
|
||||
console.log(prompt);
|
||||
await tick();
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
if (chatInputElement) {
|
||||
chatInputElement.style.height = '';
|
||||
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const eventDispatch = createEventDispatcher();
|
||||
|
||||
import { EditorState, Plugin } from 'prosemirror-state';
|
||||
import { EditorState, Plugin, TextSelection } from 'prosemirror-state';
|
||||
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { undo, redo, history } from 'prosemirror-history';
|
||||
import { schema, defaultMarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
@ -24,6 +24,7 @@
|
||||
export let className = 'input-prose';
|
||||
export let shiftEnter = false;
|
||||
|
||||
export let id = '';
|
||||
export let value = '';
|
||||
export let placeholder = 'Type here...';
|
||||
|
||||
@ -189,7 +190,7 @@
|
||||
|
||||
Enter: (state, dispatch, view) => {
|
||||
if (shiftEnter) {
|
||||
eventDispatch('submit');
|
||||
eventDispatch('enter');
|
||||
return true;
|
||||
}
|
||||
return chainCommands(
|
||||
@ -279,10 +280,40 @@
|
||||
return false;
|
||||
},
|
||||
paste: (view, event) => {
|
||||
console.log(event);
|
||||
if (event.clipboardData) {
|
||||
// Check if the pasted content contains image files
|
||||
const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
|
||||
file.type.startsWith('image/')
|
||||
);
|
||||
|
||||
// Check for image in dataTransfer items (for cases where files are not available)
|
||||
const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
|
||||
item.type.startsWith('image/')
|
||||
);
|
||||
|
||||
console.log('Has image file:', hasImageFile, 'Has image item:', hasImageItem);
|
||||
|
||||
if (hasImageFile) {
|
||||
// If there's an image, dispatch the event to the parent
|
||||
eventDispatch('paste', { event });
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasImageItem) {
|
||||
// If there's an image item, dispatch the event to the parent
|
||||
eventDispatch('paste', { event });
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For all other cases (text, formatted text, etc.), let ProseMirror handle it
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
attributes: { id }
|
||||
});
|
||||
});
|
||||
|
||||
@ -292,7 +323,8 @@
|
||||
const newState = EditorState.create({
|
||||
doc: newDoc,
|
||||
schema,
|
||||
plugins: view.state.plugins
|
||||
plugins: view.state.plugins,
|
||||
selection: TextSelection.atEnd(newDoc) // This sets the cursor at the end
|
||||
});
|
||||
view.updateState(newState);
|
||||
}
|
||||
|
@ -276,7 +276,8 @@ export const removeLastWordFromString = (inputString, wordString) => {
|
||||
// Split the string into an array of words
|
||||
const words = inputString.split(' ');
|
||||
|
||||
if (words.at(-1) === wordString) {
|
||||
console.log(words.at(-1), wordString);
|
||||
if (words.at(-1) === wordString || (wordString === '' && words.at(-1) === '\\#')) {
|
||||
words.pop();
|
||||
}
|
||||
|
||||
|
@ -137,7 +137,7 @@
|
||||
if (isShiftPressed && event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
console.log('focusInput');
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
}
|
||||
|
||||
// Check if Ctrl + Shift + ; is pressed
|
||||
|
@ -23,7 +23,7 @@
|
||||
fill: #ebbcba;
|
||||
}
|
||||
|
||||
.rose-pine-dawn #chat-textarea {
|
||||
.rose-pine-dawn #chat-input {
|
||||
background: #cecacd;
|
||||
margin: 0.3rem;
|
||||
padding: 0.5rem;
|
||||
|
@ -23,7 +23,7 @@
|
||||
fill: #c4a7e7;
|
||||
}
|
||||
|
||||
.rose-pine #chat-textarea {
|
||||
.rose-pine #chat-input {
|
||||
background: #393552;
|
||||
margin: 0.3rem;
|
||||
padding: 0.5rem;
|
||||
|
Loading…
Reference in New Issue
Block a user