ソースを参照

enh: formatting toolbar for chat

Timothy Jaeryang Baek 2 ヶ月 前
コミット
bfa42c6277

+ 2 - 2
src/lib/components/channel/MessageInput.svelte

@@ -60,7 +60,7 @@
 	export let scrollToBottom: Function = () => {};
 
 	export let acceptFiles = true;
-	export let showFormattingButtons = true;
+	export let showFormattingToolbar = true;
 
 	let showInputVariablesModal = false;
 	let inputVariables: Record<string, any> = {};
@@ -700,7 +700,7 @@
 									bind:this={chatInputElement}
 									json={true}
 									messageInput={true}
-									{showFormattingButtons}
+									{showFormattingToolbar}
 									shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
 										(!$mobile ||
 											!(

+ 203 - 188
src/lib/components/chat/MessageInput.svelte

@@ -1086,232 +1086,247 @@
 											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"
 										>
-											<RichTextInput
-												bind:this={chatInputElement}
-												id="chat-input"
-												onChange={(e) => {
-													prompt = e.md;
-													command = getCommand();
-												}}
-												json={true}
-												messageInput={true}
-												showFormattingButtons={false}
-												insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false}
-												shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
-													(!$mobile ||
-														!(
-															'ontouchstart' in window ||
-															navigator.maxTouchPoints > 0 ||
-															navigator.msMaxTouchPoints > 0
-														))}
-												placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
-												largeTextAsFile={($settings?.largeTextAsFile ?? false) && !shiftKey}
-												autocomplete={$config?.features?.enable_autocomplete_generation &&
-													($settings?.promptAutocomplete ?? false)}
-												generateAutoCompletion={async (text) => {
-													if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
-														toast.error($i18n.t('Please select a model first.'));
-													}
-
-													const res = await generateAutoCompletion(
-														localStorage.token,
-														selectedModelIds.at(0),
-														text,
-														history?.currentId
-															? createMessagesList(history, history.currentId)
-															: null
-													).catch((error) => {
-														console.log(error);
-
-														return null;
-													});
-
-													console.log(res);
-													return res;
-												}}
-												oncompositionstart={() => (isComposing = true)}
-												oncompositionend={() => (isComposing = false)}
-												on:keydown={async (e) => {
-													e = e.detail.event;
-
-													const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
-													const commandsContainerElement =
-														document.getElementById('commands-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();
-													}
+											{#key $settings?.showFormattingToolbar ?? false}
+												<RichTextInput
+													bind:this={chatInputElement}
+													id="chat-input"
+													onChange={(e) => {
+														prompt = e.md;
+														command = getCommand();
+													}}
+													json={true}
+													messageInput={true}
+													showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
+													floatingMenuPlacement={'top-start'}
+													insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false}
+													shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
+														(!$mobile ||
+															!(
+																'ontouchstart' in window ||
+																navigator.maxTouchPoints > 0 ||
+																navigator.msMaxTouchPoints > 0
+															))}
+													placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
+													largeTextAsFile={($settings?.largeTextAsFile ?? false) && !shiftKey}
+													autocomplete={$config?.features?.enable_autocomplete_generation &&
+														($settings?.promptAutocomplete ?? false)}
+													generateAutoCompletion={async (text) => {
+														if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
+															toast.error($i18n.t('Please select a model first.'));
+														}
 
-													if (prompt === '' && e.key == 'ArrowUp') {
-														e.preventDefault();
+														const res = await generateAutoCompletion(
+															localStorage.token,
+															selectedModelIds.at(0),
+															text,
+															history?.currentId
+																? createMessagesList(history, history.currentId)
+																: null
+														).catch((error) => {
+															console.log(error);
+
+															return null;
+														});
 
-														const userMessageElement = [
-															...document.getElementsByClassName('user-message')
-														]?.at(-1);
+														console.log(res);
+														return res;
+													}}
+													oncompositionstart={() => (isComposing = true)}
+													oncompositionend={() => (isComposing = false)}
+													on:keydown={async (e) => {
+														e = e.detail.event;
 
-														if (userMessageElement) {
-															userMessageElement.scrollIntoView({ block: 'center' });
-															const editButton = [
-																...document.getElementsByClassName('edit-user-message-button')
-															]?.at(-1);
+														const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
+														const commandsContainerElement =
+															document.getElementById('commands-container');
 
-															editButton?.click();
+														if (e.key === 'Escape') {
+															stopResponse();
 														}
-													}
 
-													if (commandsContainerElement) {
-														if (commandsContainerElement && e.key === 'ArrowUp') {
+														// Command/Ctrl + Shift + Enter to submit a message pair
+														if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
 															e.preventDefault();
-															commandsElement.selectUp();
-
-															const commandOptionButton = [
-																...document.getElementsByClassName('selected-command-option-button')
-															]?.at(-1);
-															commandOptionButton.scrollIntoView({ block: 'center' });
+															createMessagePair(prompt);
 														}
 
-														if (commandsContainerElement && e.key === 'ArrowDown') {
+														// Check if Ctrl + R is pressed
+														if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
 															e.preventDefault();
-															commandsElement.selectDown();
+															console.log('regenerate');
 
-															const commandOptionButton = [
-																...document.getElementsByClassName('selected-command-option-button')
+															const regenerateButton = [
+																...document.getElementsByClassName('regenerate-response-button')
 															]?.at(-1);
-															commandOptionButton.scrollIntoView({ block: 'center' });
+
+															regenerateButton?.click();
 														}
 
-														if (commandsContainerElement && e.key === 'Tab') {
+														if (prompt === '' && e.key == 'ArrowUp') {
 															e.preventDefault();
 
-															const commandOptionButton = [
-																...document.getElementsByClassName('selected-command-option-button')
+															const userMessageElement = [
+																...document.getElementsByClassName('user-message')
 															]?.at(-1);
 
-															commandOptionButton?.click();
+															if (userMessageElement) {
+																userMessageElement.scrollIntoView({ block: 'center' });
+																const editButton = [
+																	...document.getElementsByClassName('edit-user-message-button')
+																]?.at(-1);
+
+																editButton?.click();
+															}
 														}
 
-														if (commandsContainerElement && e.key === 'Enter') {
-															e.preventDefault();
+														if (commandsContainerElement) {
+															if (commandsContainerElement && e.key === 'ArrowUp') {
+																e.preventDefault();
+																commandsElement.selectUp();
+
+																const commandOptionButton = [
+																	...document.getElementsByClassName(
+																		'selected-command-option-button'
+																	)
+																]?.at(-1);
+																commandOptionButton.scrollIntoView({ block: 'center' });
+															}
 
-															const commandOptionButton = [
-																...document.getElementsByClassName('selected-command-option-button')
-															]?.at(-1);
+															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 === 'Tab') {
+																e.preventDefault();
+
+																const commandOptionButton = [
+																	...document.getElementsByClassName(
+																		'selected-command-option-button'
+																	)
+																]?.at(-1);
 
-															if (commandOptionButton) {
 																commandOptionButton?.click();
-															} else {
-																document.getElementById('send-message-button')?.click();
-															}
-														}
-													} else {
-														if (
-															!$mobile ||
-															!(
-																'ontouchstart' in window ||
-																navigator.maxTouchPoints > 0 ||
-																navigator.msMaxTouchPoints > 0
-															)
-														) {
-															if (isComposing) {
-																return;
 															}
 
-															// Uses keyCode '13' for Enter key for chinese/japanese keyboards.
-															//
-															// Depending on the user's settings, it will send the message
-															// either when Enter is pressed or when Ctrl+Enter is pressed.
-															const enterPressed =
-																($settings?.ctrlEnterToSend ?? false)
-																	? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
-																	: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
-
-															if (enterPressed) {
+															if (commandsContainerElement && e.key === 'Enter') {
 																e.preventDefault();
-																if (prompt !== '' || files.length > 0) {
-																	dispatch('submit', prompt);
+
+																const commandOptionButton = [
+																	...document.getElementsByClassName(
+																		'selected-command-option-button'
+																	)
+																]?.at(-1);
+
+																if (commandOptionButton) {
+																	commandOptionButton?.click();
+																} else {
+																	document.getElementById('send-message-button')?.click();
 																}
 															}
-														}
-													}
+														} else {
+															if (
+																!$mobile ||
+																!(
+																	'ontouchstart' in window ||
+																	navigator.maxTouchPoints > 0 ||
+																	navigator.msMaxTouchPoints > 0
+																)
+															) {
+																if (isComposing) {
+																	return;
+																}
 
-													if (e.key === 'Escape') {
-														console.log('Escape');
-														atSelectedModel = undefined;
-														selectedToolIds = [];
-														selectedFilterIds = [];
+																// Uses keyCode '13' for Enter key for chinese/japanese keyboards.
+																//
+																// Depending on the user's settings, it will send the message
+																// either when Enter is pressed or when Ctrl+Enter is pressed.
+																const enterPressed =
+																	($settings?.ctrlEnterToSend ?? false)
+																		? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
+																		: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
 
-														webSearchEnabled = false;
-														imageGenerationEnabled = false;
-														codeInterpreterEnabled = false;
-													}
-												}}
-												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);
-															} else if (item?.kind === 'file') {
-																const file = item.getAsFile();
-																if (file) {
-																	const _files = [file];
-																	await inputFilesHandler(_files);
+																if (enterPressed) {
 																	e.preventDefault();
+																	if (prompt !== '' || files.length > 0) {
+																		dispatch('submit', prompt);
+																	}
 																}
-															} 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'
-																		});
+														if (e.key === 'Escape') {
+															console.log('Escape');
+															atSelectedModel = undefined;
+															selectedToolIds = [];
+															selectedFilterIds = [];
+
+															webSearchEnabled = false;
+															imageGenerationEnabled = false;
+															codeInterpreterEnabled = false;
+														}
+													}}
+													on:paste={async (e) => {
+														e = e.detail.event;
+														console.log(e);
+
+														const clipboardData = e.clipboardData || window.clipboardData;
 
-																		await uploadFileHandler(file, true);
+														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);
+																} 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);
+																		}
 																	}
 																}
 															}
 														}
-													}
-												}}
-											/>
+													}}
+												/>
+											{/key}
 										</div>
 									{:else}
 										<textarea

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

@@ -38,6 +38,7 @@
 	let detectArtifacts = true;
 
 	let richTextInput = true;
+	let showFormattingToolbar = false;
 	let insertPromptAsRichText = false;
 	let promptAutocomplete = false;
 
@@ -228,6 +229,11 @@
 		saveSettings({ richTextInput });
 	};
 
+	const toggleShowFormattingToolbar = async () => {
+		showFormattingToolbar = !showFormattingToolbar;
+		saveSettings({ showFormattingToolbar });
+	};
+
 	const toggleInsertPromptAsRichText = async () => {
 		insertPromptAsRichText = !insertPromptAsRichText;
 		saveSettings({ insertPromptAsRichText });
@@ -335,6 +341,7 @@
 		chatFadeStreamingText = $settings?.chatFadeStreamingText ?? true;
 
 		richTextInput = $settings?.richTextInput ?? true;
+		showFormattingToolbar = $settings?.showFormattingToolbar ?? false;
 		insertPromptAsRichText = $settings?.insertPromptAsRichText ?? false;
 		promptAutocomplete = $settings?.promptAutocomplete ?? false;
 
@@ -863,6 +870,29 @@
 			</div>
 
 			{#if richTextInput}
+				<div>
+					<div class=" py-0.5 flex w-full justify-between">
+						<div id="rich-input-label" class=" self-center text-xs">
+							{$i18n.t('Show Formatting Toolbar')}
+						</div>
+
+						<button
+							aria-labelledby="rich-input-label"
+							class="p-1 px-3 text-xs flex rounded-sm transition"
+							on:click={() => {
+								toggleShowFormattingToolbar();
+							}}
+							type="button"
+						>
+							{#if showFormattingToolbar === 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 id="rich-input-label" class=" self-center text-xs">

+ 3 - 3
src/lib/components/common/RichTextInput.svelte

@@ -222,7 +222,7 @@
 	export let editable = true;
 	export let collaboration = false;
 
-	export let showFormattingButtons = true;
+	export let showFormattingToolbar = true;
 
 	export let preserveBreaks = false;
 	export let generateAutoCompletion: Function = async () => null;
@@ -1003,7 +1003,7 @@
 						]
 					: []),
 
-				...(showFormattingButtons
+				...(showFormattingToolbar
 					? [
 							BubbleMenu.configure({
 								element: bubbleMenuElement,
@@ -1316,7 +1316,7 @@
 	};
 </script>
 
-{#if showFormattingButtons}
+{#if showFormattingToolbar}
 	<div bind:this={bubbleMenuElement} id="bubble-menu" class="p-0">
 		<FormattingButtons {editor} />
 	</div>

+ 1 - 1
src/lib/components/notes/NoteEditor/Chat.svelte

@@ -390,7 +390,7 @@ Based on the user's instruction, update and enhance the existing notes or select
 						bind:chatInputElement
 						acceptFiles={false}
 						inputLoading={loading}
-						showFormattingButtons={false}
+						showFormattingToolbar={false}
 						onSubmit={submitHandler}
 						{onStop}
 					>