浏览代码

enh: rich text input toggle option

Timothy J. Baek 6 月之前
父节点
当前提交
adede5480d

+ 221 - 46
src/lib/components/chat/MessageInput.svelte

@@ -509,54 +509,202 @@
 									</InputMenu>
 								</div>
 
-								<div
-									bind:this={chatInputContainerElement}
-									id="chat-input-container"
-									class="scrollbar-hidden text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-[48px] overflow-auto"
-								>
-									<RichTextInput
-										bind:this={chatInputElement}
+								{#if $settings?.richTextInput ?? true}
+									<div
+										bind:this={chatInputContainerElement}
+										id="chat-input-container"
+										class="scrollbar-hidden text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-[48px] overflow-auto"
+									>
+										<RichTextInput
+											bind:this={chatInputElement}
+											id="chat-input"
+											trim={true}
+											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;
+											}}
+											on:keydown={async (e) => {
+												e = e.detail.event;
+
+												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();
+												}
+
+												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>
+								{:else}
+									<textarea
 										id="chat-input"
-										trim={true}
+										bind:this={chatInputElement}
+										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}
-										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;
-										}}
-										on:keydown={async (e) => {
-											e = e.detail.event;
+											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();
+												}
 
-											if (chatInputContainerElement) {
-												chatInputContainerElement.style.height = '';
-												chatInputContainerElement.style.height =
-													Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+												// Submit the prompt when Enter key is pressed
+												if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
+													dispatch('submit', prompt);
+												}
 											}
-
+										}}
+										on:keydown={async (e) => {
 											const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
 											const commandsContainerElement =
 												document.getElementById('commands-container');
@@ -640,6 +788,26 @@
 												]?.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') {
@@ -647,10 +815,17 @@
 												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) {
@@ -675,7 +850,7 @@
 											}
 										}}
 									/>
-								</div>
+								{/if}
 
 								<div class="self-end mb-2 flex space-x-1 mr-1">
 									{#if !history?.currentId || history.messages[history.currentId]?.done == true}

+ 5 - 2
src/lib/components/chat/MessageInput/Commands/Prompts.svelte

@@ -120,13 +120,16 @@
 		const chatInputElement = document.getElementById('chat-input');
 
 		await tick();
-
 		if (chatInputContainerElement) {
 			chatInputContainerElement.style.height = '';
 			chatInputContainerElement.style.height =
 				Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+		}
 
-			chatInputElement?.focus();
+		await tick();
+		if (chatInputElement) {
+			chatInputElement.focus();
+			chatInputElement.dispatchEvent(new Event('input'));
 		}
 	};
 </script>

+ 7 - 2
src/lib/components/chat/Placeholder.svelte

@@ -58,13 +58,18 @@
 		await tick();
 
 		const chatInputContainerElement = document.getElementById('chat-input-container');
+		const chatInputElement = document.getElementById('chat-input');
+
 		if (chatInputContainerElement) {
 			chatInputContainerElement.style.height = '';
 			chatInputContainerElement.style.height =
 				Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
+		}
 
-			const chatInputElement = document.getElementById('chat-input');
-			chatInputElement?.focus();
+		await tick();
+		if (chatInputElement) {
+			chatInputElement.focus();
+			chatInputElement.dispatchEvent(new Event('input'));
 		}
 
 		await tick();

+ 29 - 0
src/lib/components/chat/Settings/Interface.svelte

@@ -30,6 +30,7 @@
 	// Interface
 	let defaultModelId = '';
 	let showUsername = false;
+	let richTextInput = true;
 
 	let landingPageMode = '';
 	let chatBubble = true;
@@ -125,6 +126,11 @@
 		saveSettings({ autoTags });
 	};
 
+	const toggleRichTextInput = async () => {
+		richTextInput = !richTextInput;
+		saveSettings({ richTextInput });
+	};
+
 	const toggleResponseAutoCopy = async () => {
 		const permission = await navigator.clipboard
 			.readText()
@@ -172,6 +178,7 @@
 		showEmojiInCall = $settings.showEmojiInCall ?? false;
 		voiceInterruption = $settings.voiceInterruption ?? false;
 
+		richTextInput = $settings.richTextInput ?? true;
 		landingPageMode = $settings.landingPageMode ?? '';
 		chatBubble = $settings.chatBubble ?? true;
 		widescreenMode = $settings.widescreenMode ?? false;
@@ -422,6 +429,28 @@
 				</div>
 			</div>
 
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">
+						{$i18n.t('Rich Text Input for Chat')}
+					</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleRichTextInput();
+						}}
+						type="button"
+					>
+						{#if richTextInput === 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>
+
 			<div>
 				<div class=" py-0.5 flex w-full justify-between">
 					<div class=" self-center text-xs">