feat: rich text input for chat

This commit is contained in:
Timothy J. Baek
2024-10-18 23:54:35 -07:00
parent 5e96922eba
commit f46b95300b
15 changed files with 268 additions and 178 deletions

View File

@@ -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 });
}

View File

@@ -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,8 +504,195 @@
</InputMenu>
</div>
<textarea
id="chat-textarea"
<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}
shiftEnter={!$mobile ||
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)}
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');
// 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 (commandsContainerElement && e.key === 'ArrowUp') {
e.preventDefault();
commandsElement.selectUp();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (commandsContainerElement && e.key === 'ArrowDown') {
e.preventDefault();
commandsElement.selectDown();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (commandsContainerElement && e.key === 'Enter') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
if (e.shiftKey) {
prompt = `${prompt}\n`;
} else if (commandOptionButton) {
commandOptionButton?.click();
} else {
document.getElementById('send-message-button')?.click();
}
}
if (commandsContainerElement && e.key === 'Tab') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton?.click();
} else if (e.key === 'Tab') {
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
const fullPrompt = prompt;
prompt = prompt.substring(0, word?.endIndex + 1);
await tick();
e.target.scrollTop = e.target.scrollHeight;
prompt = fullPrompt;
await tick();
e.preventDefault();
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
e.target.style.height = '';
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}
if (e.key === 'Escape') {
console.log('Escape');
atSelectedModel = undefined;
}
}}
on:paste={async (e) => {
e = e.detail.event;
console.log(e);
const clipboardData = e.clipboardData || window.clipboardData;
if (clipboardData && clipboardData.items) {
for (const item of clipboardData.items) {
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);
}
}
}
}}
/>
</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')}
@@ -526,151 +717,12 @@
}
}
}}
on:keydown={async (e) => {
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
const commandsContainerElement = document.getElementById('commands-container');
// 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 (commandsContainerElement && e.key === 'ArrowUp') {
e.preventDefault();
commandsElement.selectUp();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (commandsContainerElement && e.key === 'ArrowDown') {
e.preventDefault();
commandsElement.selectDown();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (commandsContainerElement && e.key === 'Enter') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
if (e.shiftKey) {
prompt = `${prompt}\n`;
} else if (commandOptionButton) {
commandOptionButton?.click();
} else {
document.getElementById('send-message-button')?.click();
}
}
if (commandsContainerElement && e.key === 'Tab') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton?.click();
} else if (e.key === 'Tab') {
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
const fullPrompt = prompt;
prompt = prompt.substring(0, word?.endIndex + 1);
await tick();
e.target.scrollTop = e.target.scrollHeight;
prompt = fullPrompt;
await tick();
e.preventDefault();
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
e.target.style.height = '';
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}
if (e.key === 'Escape') {
console.log('Escape');
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) => {
const clipboardData = e.clipboardData || window.clipboardData;
if (clipboardData && clipboardData.items) {
for (const item of clipboardData.items) {
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);
}
}
}
}}
/>
/> -->
<div class="self-end mb-2 flex space-x-1 mr-1">
{#if !history?.currentId || history.messages[history.currentId]?.done == true}

View File

@@ -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', {

View File

@@ -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();

View File

@@ -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();

View File

@@ -110,7 +110,7 @@
prompt = text;
const chatInputElement = document.getElementById('chat-textarea');
const chatInputElement = document.getElementById('chat-input');
await tick();

View File

@@ -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(() => {

View File

@@ -330,7 +330,7 @@
await tick();
const chatInputElement = document.getElementById('chat-textarea');
const chatInputElement = document.getElementById('chat-input');
if (chatInputElement) {
prompt = p;

View File

@@ -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';