Timothy Jaeryang Baek пре 3 недеља
родитељ
комит
06c1426e14

+ 456 - 373
src/lib/components/channel/MessageInput.svelte

@@ -17,9 +17,12 @@
 		getFormattedTime,
 		getUserPosition,
 		getUserTimezone,
-		getWeekday
+		getWeekday,
+		extractCurlyBraceWords
 	} from '$lib/utils';
 
+	import { getSessionUser } from '$lib/apis/auths';
+
 	import Tooltip from '../common/Tooltip.svelte';
 	import RichTextInput from '../common/RichTextInput.svelte';
 	import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
@@ -29,26 +32,16 @@
 	import FileItem from '../common/FileItem.svelte';
 	import Image from '../common/Image.svelte';
 	import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
-	import Commands from '../chat/MessageInput/Commands.svelte';
 	import InputVariablesModal from '../chat/MessageInput/InputVariablesModal.svelte';
-	import { getSessionUser } from '$lib/apis/auths';
+	import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
+	import CommandSuggestionList from '../chat/MessageInput/CommandSuggestionList.svelte';
+	import MentionList from './MessageInput/MentionList.svelte';
 
 	export let placeholder = $i18n.t('Send a Message');
 
 	export let id = null;
-
-	let draggedOver = false;
-
-	let recording = false;
-	let content = '';
-	let files = [];
-
 	export let chatInputElement;
 
-	let commandsElement;
-	let filesInputElement;
-	let inputFiles;
-
 	export let typingUsers = [];
 	export let inputLoading = false;
 
@@ -62,15 +55,39 @@
 	export let acceptFiles = true;
 	export let showFormattingToolbar = true;
 
+	let loaded = false;
+	let draggedOver = false;
+
+	let recording = false;
+	let content = '';
+	let files = [];
+
+	let filesInputElement;
+	let inputFiles;
+
 	let showInputVariablesModal = false;
+	let inputVariablesModalCallback: (variableValues: Record<string, any>) => void;
 	let inputVariables: Record<string, any> = {};
 	let inputVariableValues = {};
 
-	const inputVariableHandler = async (text: string) => {
+	const inputVariableHandler = async (text: string): Promise<string> => {
 		inputVariables = extractInputVariables(text);
-		if (Object.keys(inputVariables).length > 0) {
-			showInputVariablesModal = true;
+
+		// No variables? return the original text immediately.
+		if (Object.keys(inputVariables).length === 0) {
+			return text;
 		}
+
+		// Show modal and wait for the user's input.
+		showInputVariablesModal = true;
+		return await new Promise<string>((resolve) => {
+			inputVariablesModalCallback = (variableValues) => {
+				inputVariableValues = { ...inputVariableValues, ...variableValues };
+				replaceVariables(inputVariableValues);
+				showInputVariablesModal = false;
+				resolve(text);
+			};
+		});
 	};
 
 	const textVariableHandler = async (text: string) => {
@@ -188,68 +205,87 @@
 			text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
 		}
 
-		inputVariableHandler(text);
 		return text;
 	};
 
 	const replaceVariables = (variables: Record<string, any>) => {
-		if (!chatInputElement) return;
 		console.log('Replacing variables:', variables);
 
-		chatInputElement.replaceVariables(variables);
-		chatInputElement.focus();
+		const chatInput = document.getElementById('chat-input');
+
+		if (chatInput) {
+			chatInputElement.replaceVariables(variables);
+			chatInputElement.focus();
+		}
 	};
 
-	export const setText = async (text?: string) => {
-		if (!chatInputElement) return;
+	export const setText = async (text?: string, cb?: (text: string) => void) => {
+		const chatInput = document.getElementById('chat-input');
+
+		if (chatInput) {
+			text = await textVariableHandler(text || '');
 
-		text = await textVariableHandler(text || '');
+			chatInputElement?.setText(text);
+			chatInputElement?.focus();
 
-		chatInputElement?.setText(text);
-		chatInputElement?.focus();
+			text = await inputVariableHandler(text);
+			await tick();
+			if (cb) await cb(text);
+		}
 	};
 
 	const getCommand = () => {
-		if (!chatInputElement) return;
-
+		const chatInput = document.getElementById('chat-input');
 		let word = '';
-		word = chatInputElement?.getWordAtDocPos();
+
+		if (chatInput) {
+			word = chatInputElement?.getWordAtDocPos();
+		}
 
 		return word;
 	};
 
 	const replaceCommandWithText = (text) => {
-		if (!chatInputElement) return;
+		const chatInput = document.getElementById('chat-input');
+		if (!chatInput) return;
 
 		chatInputElement?.replaceCommandWithText(text);
 	};
 
 	const insertTextAtCursor = async (text: string) => {
+		const chatInput = document.getElementById('chat-input');
+		if (!chatInput) return;
+
 		text = await textVariableHandler(text);
 
 		if (command) {
 			replaceCommandWithText(text);
 		} else {
-			const selection = window.getSelection();
-			if (selection && selection.rangeCount > 0) {
-				const range = selection.getRangeAt(0);
-				range.deleteContents();
-				range.insertNode(document.createTextNode(text));
-				range.collapse(false);
-				selection.removeAllRanges();
-				selection.addRange(range);
-			}
+			chatInputElement?.insertContent(text);
 		}
 
 		await tick();
+		text = await inputVariableHandler(text);
+		await tick();
+
 		const chatInputContainer = document.getElementById('chat-input-container');
 		if (chatInputContainer) {
 			chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
 		}
 
 		await tick();
-		if (chatInputElement) {
-			chatInputElement.focus();
+		if (chatInput) {
+			chatInput.focus();
+			chatInput.dispatchEvent(new Event('input'));
+
+			const words = extractCurlyBraceWords(prompt);
+
+			if (words.length > 0) {
+				const word = words.at(0);
+				await tick();
+			} else {
+				chatInput.scrollTop = chatInput.scrollHeight;
+			}
 		}
 	};
 
@@ -257,6 +293,7 @@
 
 	export let showCommands = false;
 	$: showCommands = ['/'].includes(command?.charAt(0));
+	let suggestions = null;
 
 	const screenCaptureHandler = async () => {
 		try {
@@ -514,6 +551,49 @@
 	}
 
 	onMount(async () => {
+		suggestions = [
+			{
+				char: '@',
+				render: getSuggestionRenderer(MentionList, {
+					i18n
+				})
+			},
+			{
+				char: '/',
+				render: getSuggestionRenderer(CommandSuggestionList, {
+					i18n,
+					onSelect: (e) => {
+						const { type, data } = e;
+
+						if (type === 'model') {
+							console.log('Selected model:', data);
+						}
+
+						document.getElementById('chat-input')?.focus();
+					},
+
+					insertTextHandler: insertTextAtCursor,
+					onUpload: (e) => {
+						const { type, data } = e;
+
+						if (type === 'file') {
+							if (files.find((f) => f.id === data.id)) {
+								return;
+							}
+							files = [
+								...files,
+								{
+									...data,
+									status: 'processed'
+								}
+							];
+						}
+					}
+				})
+			}
+		];
+		loaded = true;
+
 		window.setTimeout(() => {
 			if (chatInputElement) {
 				chatInputElement.focus();
@@ -543,389 +623,392 @@
 	});
 </script>
 
-<FilesOverlay show={draggedOver} />
+{#if loaded}
+	<FilesOverlay show={draggedOver} />
+
+	{#if acceptFiles}
+		<input
+			bind:this={filesInputElement}
+			bind:files={inputFiles}
+			type="file"
+			hidden
+			multiple
+			on:change={async () => {
+				if (inputFiles && inputFiles.length > 0) {
+					inputFilesHandler(Array.from(inputFiles));
+				} else {
+					toast.error($i18n.t(`File not found.`));
+				}
 
-{#if acceptFiles}
-	<input
-		bind:this={filesInputElement}
-		bind:files={inputFiles}
-		type="file"
-		hidden
-		multiple
-		on:change={async () => {
-			if (inputFiles && inputFiles.length > 0) {
-				inputFilesHandler(Array.from(inputFiles));
-			} else {
-				toast.error($i18n.t(`File not found.`));
-			}
+				filesInputElement.value = '';
+			}}
+		/>
+	{/if}
 
-			filesInputElement.value = '';
-		}}
+	<InputVariablesModal
+		bind:show={showInputVariablesModal}
+		variables={inputVariables}
+		onSave={inputVariablesModalCallback}
 	/>
-{/if}
 
-<InputVariablesModal
-	bind:show={showInputVariablesModal}
-	variables={inputVariables}
-	onSave={(variableValues) => {
-		inputVariableValues = { ...inputVariableValues, ...variableValues };
-		replaceVariables(inputVariableValues);
-	}}
-/>
-
-<div class="bg-transparent">
-	<div
-		class="{($settings?.widescreenMode ?? null)
-			? 'max-w-full'
-			: 'max-w-6xl'}  mx-auto inset-x-0 relative"
-	>
-		<div class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center">
-			<div class="flex flex-col px-3 w-full">
-				<div class="relative">
-					{#if scrollEnd === false}
-						<div
-							class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
-						>
-							<button
-								class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
-								on:click={() => {
-									scrollEnd = true;
-									scrollToBottom();
-								}}
+	<div class="bg-transparent">
+		<div
+			class="{($settings?.widescreenMode ?? null)
+				? 'max-w-full'
+				: 'max-w-6xl'}  mx-auto inset-x-0 relative"
+		>
+			<div
+				class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center"
+			>
+				<div class="flex flex-col px-3 w-full">
+					<div class="relative">
+						{#if scrollEnd === false}
+							<div
+								class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
 							>
-								<svg
-									xmlns="http://www.w3.org/2000/svg"
-									viewBox="0 0 20 20"
-									fill="currentColor"
-									class="w-5 h-5"
+								<button
+									class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
+									on:click={() => {
+										scrollEnd = true;
+										scrollToBottom();
+									}}
 								>
-									<path
-										fill-rule="evenodd"
-										d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
-										clip-rule="evenodd"
-									/>
-								</svg>
-							</button>
-						</div>
-					{/if}
-				</div>
-
-				<div class="relative">
-					<div class=" -mt-5">
-						{#if typingUsers.length > 0}
-							<div class=" text-xs px-4 mb-1">
-								<span class=" font-normal text-black dark:text-white">
-									{typingUsers.map((user) => user.name).join(', ')}
-								</span>
-								{$i18n.t('is typing...')}
+									<svg
+										xmlns="http://www.w3.org/2000/svg"
+										viewBox="0 0 20 20"
+										fill="currentColor"
+										class="w-5 h-5"
+									>
+										<path
+											fill-rule="evenodd"
+											d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
+											clip-rule="evenodd"
+										/>
+									</svg>
+								</button>
 							</div>
 						{/if}
 					</div>
 
-					<Commands
-						bind:this={commandsElement}
-						show={showCommands}
-						{command}
-						insertTextHandler={insertTextAtCursor}
-					/>
+					<div class="relative">
+						<div class=" -mt-5">
+							{#if typingUsers.length > 0}
+								<div class=" text-xs px-4 mb-1">
+									<span class=" font-normal text-black dark:text-white">
+										{typingUsers.map((user) => user.name).join(', ')}
+									</span>
+									{$i18n.t('is typing...')}
+								</div>
+							{/if}
+						</div>
+					</div>
 				</div>
 			</div>
-		</div>
 
-		<div class="">
-			{#if recording}
-				<VoiceRecording
-					bind:recording
-					onCancel={async () => {
-						recording = false;
+			<div class="">
+				{#if recording}
+					<VoiceRecording
+						bind:recording
+						onCancel={async () => {
+							recording = false;
 
-						await tick();
+							await tick();
 
-						if (chatInputElement) {
-							chatInputElement.focus();
-						}
-					}}
-					onConfirm={async (data) => {
-						const { text, filename } = data;
-						recording = false;
+							if (chatInputElement) {
+								chatInputElement.focus();
+							}
+						}}
+						onConfirm={async (data) => {
+							const { text, filename } = data;
+							recording = false;
 
-						await tick();
-						insertTextAtCursor(text);
+							await tick();
+							insertTextAtCursor(text);
 
-						await tick();
+							await tick();
 
-						if (chatInputElement) {
-							chatInputElement.focus();
-						}
-					}}
-				/>
-			{:else}
-				<form
-					class="w-full flex gap-1.5"
-					on:submit|preventDefault={() => {
-						submitHandler();
-					}}
-				>
-					<div
-						class="flex-1 flex flex-col relative w-full rounded-3xl px-1 bg-gray-600/5 dark:bg-gray-400/5 dark:text-gray-100"
-						dir={$settings?.chatDirection ?? 'auto'}
+							if (chatInputElement) {
+								chatInputElement.focus();
+							}
+						}}
+					/>
+				{:else}
+					<form
+						class="w-full flex gap-1.5"
+						on:submit|preventDefault={() => {
+							submitHandler();
+						}}
 					>
-						{#if files.length > 0}
-							<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
-								{#each files as file, fileIdx}
-									{#if file.type === 'image'}
-										<div class=" relative group">
-											<div class="relative">
-												<Image
-													src={file.url}
-													alt="input"
-													imageClassName=" h-16 w-16 rounded-xl object-cover"
-												/>
+						<div
+							class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
+							dir={$settings?.chatDirection ?? 'auto'}
+						>
+							{#if files.length > 0}
+								<div class="mx-2 mt-2.5 -mb-1 flex flex-wrap gap-2">
+									{#each files as file, fileIdx}
+										{#if file.type === 'image'}
+											<div class=" relative group">
+												<div class="relative">
+													<Image
+														src={file.url}
+														alt=""
+														imageClassName=" size-10 rounded-xl object-cover"
+													/>
+												</div>
+												<div class=" absolute -top-1 -right-1">
+													<button
+														class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
+														type="button"
+														on:click={() => {
+															files.splice(fileIdx, 1);
+															files = files;
+														}}
+													>
+														<svg
+															xmlns="http://www.w3.org/2000/svg"
+															viewBox="0 0 20 20"
+															fill="currentColor"
+															class="w-4 h-4"
+														>
+															<path
+																d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+															/>
+														</svg>
+													</button>
+												</div>
 											</div>
-											<div class=" absolute -top-1 -right-1">
+										{:else}
+											<FileItem
+												item={file}
+												name={file.name}
+												type={file.type}
+												size={file?.size}
+												small={true}
+												loading={file.status === 'uploading'}
+												dismissible={true}
+												edit={true}
+												on:dismiss={() => {
+													files.splice(fileIdx, 1);
+													files = files;
+												}}
+												on:click={() => {
+													console.log(file);
+												}}
+											/>
+										{/if}
+									{/each}
+								</div>
+							{/if}
+
+							<div class="px-2.5">
+								<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"
+								>
+									{#key $settings?.richTextInput}
+										<RichTextInput
+											id="chat-input"
+											bind:this={chatInputElement}
+											json={true}
+											messageInput={true}
+											richText={$settings?.richTextInput ?? true}
+											{showFormattingToolbar}
+											shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
+												(!$mobile ||
+													!(
+														'ontouchstart' in window ||
+														navigator.maxTouchPoints > 0 ||
+														navigator.msMaxTouchPoints > 0
+													))}
+											largeTextAsFile={$settings?.largeTextAsFile ?? false}
+											floatingMenuPlacement={'top-start'}
+											{suggestions}
+											onChange={(e) => {
+												const { md } = e;
+												content = md;
+												command = getCommand();
+											}}
+											on:keydown={async (e) => {
+												e = e.detail.event;
+												const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
+
+												const suggestionsContainerElement =
+													document.getElementById('suggestions-container');
+
+												if (!suggestionsContainerElement) {
+													if (
+														!$mobile ||
+														!(
+															'ontouchstart' in window ||
+															navigator.maxTouchPoints > 0 ||
+															navigator.msMaxTouchPoints > 0
+														)
+													) {
+														// Prevent Enter key from creating a new line
+														// Uses keyCode '13' for Enter key for chinese/japanese keyboards
+														if (e.keyCode === 13 && !e.shiftKey) {
+															e.preventDefault();
+														}
+
+														// Submit the content when Enter key is pressed
+														if (content !== '' && e.keyCode === 13 && !e.shiftKey) {
+															submitHandler();
+														}
+													}
+												}
+
+												if (e.key === 'Escape') {
+													console.info('Escape');
+												}
+											}}
+											on:paste={async (e) => {
+												e = e.detail.event;
+												console.info(e);
+											}}
+										/>
+									{/key}
+								</div>
+							</div>
+
+							<div class=" flex justify-between mb-2.5 mx-0.5">
+								<div class="ml-1 self-end flex space-x-1 flex-1">
+									<slot name="menu">
+										{#if acceptFiles}
+											<InputMenu
+												{screenCaptureHandler}
+												uploadFilesHandler={() => {
+													filesInputElement.click();
+												}}
+											>
 												<button
-													class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
+													class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
 													type="button"
-													on:click={() => {
-														files.splice(fileIdx, 1);
-														files = files;
-													}}
+													aria-label="More"
 												>
 													<svg
 														xmlns="http://www.w3.org/2000/svg"
 														viewBox="0 0 20 20"
 														fill="currentColor"
-														class="w-4 h-4"
+														class="size-5"
 													>
 														<path
-															d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+															d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
 														/>
 													</svg>
 												</button>
-											</div>
-										</div>
-									{:else}
-										<FileItem
-											item={file}
-											name={file.name}
-											type={file.type}
-											size={file?.size}
-											loading={file.status === 'uploading'}
-											dismissible={true}
-											edit={true}
-											on:dismiss={() => {
-												files.splice(fileIdx, 1);
-												files = files;
-											}}
-											on:click={() => {
-												console.log(file);
-											}}
-										/>
-									{/if}
-								{/each}
-							</div>
-						{/if}
-
-						<div class="px-2.5">
-							<div
-								class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
-							>
-								<RichTextInput
-									bind:this={chatInputElement}
-									json={true}
-									messageInput={true}
-									{showFormattingToolbar}
-									shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
-										(!$mobile ||
-											!(
-												'ontouchstart' in window ||
-												navigator.maxTouchPoints > 0 ||
-												navigator.msMaxTouchPoints > 0
-											))}
-									largeTextAsFile={$settings?.largeTextAsFile ?? false}
-									floatingMenuPlacement={'top-start'}
-									onChange={(e) => {
-										const { md } = e;
-										content = md;
-										command = getCommand();
-									}}
-									on:keydown={async (e) => {
-										e = e.detail.event;
-										const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
-
-										const suggestionsContainerElement =
-											document.getElementById('suggestions-container');
-
-										if (!suggestionsContainerElement) {
-											if (
-												!$mobile ||
-												!(
-													'ontouchstart' in window ||
-													navigator.maxTouchPoints > 0 ||
-													navigator.msMaxTouchPoints > 0
-												)
-											) {
-												// Prevent Enter key from creating a new line
-												// Uses keyCode '13' for Enter key for chinese/japanese keyboards
-												if (e.keyCode === 13 && !e.shiftKey) {
-													e.preventDefault();
-												}
-
-												// Submit the content when Enter key is pressed
-												if (content !== '' && e.keyCode === 13 && !e.shiftKey) {
-													submitHandler();
-												}
-											}
-										}
-
-										if (e.key === 'Escape') {
-											console.info('Escape');
-										}
-									}}
-									on:paste={async (e) => {
-										e = e.detail.event;
-										console.info(e);
-									}}
-								/>
-							</div>
-						</div>
+											</InputMenu>
+										{/if}
+									</slot>
+								</div>
 
-						<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
-							<div class="ml-1 self-end flex space-x-1 flex-1">
-								<slot name="menu">
-									{#if acceptFiles}
-										<InputMenu
-											{screenCaptureHandler}
-											uploadFilesHandler={() => {
-												filesInputElement.click();
-											}}
-										>
+								<div class="self-end flex space-x-1 mr-1">
+									{#if content === ''}
+										<Tooltip content={$i18n.t('Record voice')}>
 											<button
-												class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
+												id="voice-input-button"
+												class=" text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 mr-0.5 self-center"
 												type="button"
-												aria-label="More"
+												on:click={async () => {
+													try {
+														let stream = await navigator.mediaDevices
+															.getUserMedia({ audio: true })
+															.catch(function (err) {
+																toast.error(
+																	$i18n.t(
+																		`Permission denied when accessing microphone: {{error}}`,
+																		{
+																			error: err
+																		}
+																	)
+																);
+																return null;
+															});
+
+														if (stream) {
+															recording = true;
+															const tracks = stream.getTracks();
+															tracks.forEach((track) => track.stop());
+														}
+														stream = null;
+													} catch {
+														toast.error($i18n.t('Permission denied when accessing microphone'));
+													}
+												}}
+												aria-label="Voice Input"
 											>
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
 													viewBox="0 0 20 20"
 													fill="currentColor"
-													class="size-5"
+													class="w-5 h-5 translate-y-[0.5px]"
 												>
+													<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
 													<path
-														d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
+														d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
 													/>
 												</svg>
 											</button>
-										</InputMenu>
+										</Tooltip>
 									{/if}
-								</slot>
-							</div>
 
-							<div class="self-end flex space-x-1 mr-1">
-								{#if content === ''}
-									<Tooltip content={$i18n.t('Record voice')}>
-										<button
-											id="voice-input-button"
-											class=" text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 mr-0.5 self-center"
-											type="button"
-											on:click={async () => {
-												try {
-													let stream = await navigator.mediaDevices
-														.getUserMedia({ audio: true })
-														.catch(function (err) {
-															toast.error(
-																$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
-																	error: err
-																})
-															);
-															return null;
-														});
-
-													if (stream) {
-														recording = true;
-														const tracks = stream.getTracks();
-														tracks.forEach((track) => track.stop());
-													}
-													stream = null;
-												} catch {
-													toast.error($i18n.t('Permission denied when accessing microphone'));
-												}
-											}}
-											aria-label="Voice Input"
-										>
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												viewBox="0 0 20 20"
-												fill="currentColor"
-												class="w-5 h-5 translate-y-[0.5px]"
-											>
-												<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
-												<path
-													d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
-												/>
-											</svg>
-										</button>
-									</Tooltip>
-								{/if}
-
-								<div class=" flex items-center">
-									{#if inputLoading && onStop}
-										<div class=" flex items-center">
-											<Tooltip content={$i18n.t('Stop')}>
-												<button
-													class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
-													on:click={() => {
-														onStop();
-													}}
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														viewBox="0 0 24 24"
-														fill="currentColor"
-														class="size-5"
+									<div class=" flex items-center">
+										{#if inputLoading && onStop}
+											<div class=" flex items-center">
+												<Tooltip content={$i18n.t('Stop')}>
+													<button
+														class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
+														on:click={() => {
+															onStop();
+														}}
 													>
-														<path
-															fill-rule="evenodd"
-															d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
-															clip-rule="evenodd"
-														/>
-													</svg>
-												</button>
-											</Tooltip>
-										</div>
-									{:else}
-										<div class=" flex items-center">
-											<Tooltip content={$i18n.t('Send message')}>
-												<button
-													id="send-message-button"
-													class="{content !== '' || files.length !== 0
-														? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
-														: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
-													type="submit"
-													disabled={content === '' && files.length === 0}
-												>
-													<svg
-														xmlns="http://www.w3.org/2000/svg"
-														viewBox="0 0 16 16"
-														fill="currentColor"
-														class="size-5"
+														<svg
+															xmlns="http://www.w3.org/2000/svg"
+															viewBox="0 0 24 24"
+															fill="currentColor"
+															class="size-5"
+														>
+															<path
+																fill-rule="evenodd"
+																d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
+																clip-rule="evenodd"
+															/>
+														</svg>
+													</button>
+												</Tooltip>
+											</div>
+										{:else}
+											<div class=" flex items-center">
+												<Tooltip content={$i18n.t('Send message')}>
+													<button
+														id="send-message-button"
+														class="{content !== '' || files.length !== 0
+															? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
+															: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
+														type="submit"
+														disabled={content === '' && files.length === 0}
 													>
-														<path
-															fill-rule="evenodd"
-															d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
-															clip-rule="evenodd"
-														/>
-													</svg>
-												</button>
-											</Tooltip>
-										</div>
-									{/if}
+														<svg
+															xmlns="http://www.w3.org/2000/svg"
+															viewBox="0 0 16 16"
+															fill="currentColor"
+															class="size-5"
+														>
+															<path
+																fill-rule="evenodd"
+																d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
+																clip-rule="evenodd"
+															/>
+														</svg>
+													</button>
+												</Tooltip>
+											</div>
+										{/if}
+									</div>
 								</div>
 							</div>
 						</div>
-					</div>
-				</form>
-			{/if}
+					</form>
+				{/if}
+			</div>
 		</div>
 	</div>
-</div>
+{/if}

+ 18 - 16
src/lib/components/channel/MessageInput/InputMenu.svelte

@@ -13,6 +13,8 @@
 	import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte';
 	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
 	import CameraSolid from '$lib/components/icons/CameraSolid.svelte';
+	import Camera from '$lib/components/icons/Camera.svelte';
+	import Clip from '$lib/components/icons/Clip.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -44,34 +46,34 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[200px] rounded-xl px-1 py-1  border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
-			sideOffset={15}
-			alignOffset={-8}
-			side="top"
+			class="w-full max-w-[200px] rounded-2xl px-1 py-1  border border-gray-100  dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
+			sideOffset={4}
+			alignOffset={-6}
+			side="bottom"
 			align="start"
 			transition={flyAndScale}
 		>
+			<DropdownMenu.Item
+				class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
+				on:click={() => {
+					uploadFilesHandler();
+				}}
+			>
+				<Clip />
+				<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
+			</DropdownMenu.Item>
+
 			{#if !$mobile}
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl"
+					class="flex gap-2 items-center px-3 py-1.5 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800  rounded-xl"
 					on:click={() => {
 						screenCaptureHandler();
 					}}
 				>
-					<CameraSolid />
+					<Camera />
 					<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
 				</DropdownMenu.Item>
 			{/if}
-
-			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
-				on:click={() => {
-					uploadFilesHandler();
-				}}
-			>
-				<DocumentArrowUpSolid />
-				<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
-			</DropdownMenu.Item>
 		</DropdownMenu.Content>
 	</div>
 </Dropdown>

+ 81 - 0
src/lib/components/channel/MessageInput/MentionList.svelte

@@ -0,0 +1,81 @@
+<script lang="ts">
+	import { getContext } from 'svelte';
+	const i18n = getContext('i18n');
+
+	import { models } from '$lib/stores';
+	export let query = '';
+
+	export let command: (payload: { id: string; label: string }) => void;
+	export let selectedIndex = 0;
+
+	let items = [];
+
+	$: filteredItems = $models.filter((u) => u.name.toLowerCase().includes(query.toLowerCase()));
+
+	const select = (index: number) => {
+		const item = filteredItems[index];
+		if (item) command(item);
+	};
+
+	const onKeyDown = (event: KeyboardEvent) => {
+		if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
+
+		if (event.key === 'ArrowUp') {
+			selectedIndex = (selectedIndex + filteredItems.length - 1) % filteredItems.length;
+			const item = document.querySelector(`[data-selected="true"]`);
+			item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
+			return true;
+		}
+		if (event.key === 'ArrowDown') {
+			selectedIndex = (selectedIndex + 1) % filteredItems.length;
+			const item = document.querySelector(`[data-selected="true"]`);
+			item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
+			return true;
+		}
+		if (event.key === 'Enter' || event.key === 'Tab') {
+			select(selectedIndex);
+			return true;
+		}
+		if (event.key === 'Escape') {
+			// tell tiptap we handled it (it will close)
+			return true;
+		}
+		return false;
+	};
+
+	// This method will be called from the suggestion renderer
+	// @ts-ignore
+	export function _onKeyDown(event: KeyboardEvent) {
+		return onKeyDown(event);
+	}
+</script>
+
+{#if filteredItems.length}
+	<div
+		class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-60 p-1"
+		id="suggestions-container"
+	>
+		<div class="overflow-y-auto scrollbar-thin max-h-60">
+			<div class="px-2 text-xs text-gray-500 py-1">
+				{$i18n.t('Models')}
+			</div>
+			{#each filteredItems as item, i}
+				<button
+					type="button"
+					on:click={() => select(i)}
+					on:mousemove={() => {
+						selectedIndex = i;
+					}}
+					class="px-2.5 py-1.5 rounded-xl w-full text-left {i === selectedIndex
+						? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
+						: ''}"
+					data-selected={i === selectedIndex}
+				>
+					<div class="truncate">
+						@{item.name}
+					</div>
+				</button>
+			{/each}
+		</div>
+	</div>
+{/if}

+ 2 - 4
src/lib/components/channel/Messages/Message.svelte

@@ -138,9 +138,7 @@
 			id="message-{message.id}"
 			dir={$settings.chatDirection}
 		>
-			<div
-				class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
-			>
+			<div class={`shrink-0 mr-3 w-9`}>
 				{#if showUserProfile}
 					<ProfilePreview user={message.user}>
 						<ProfileImage
@@ -198,7 +196,7 @@
 										name={file.name}
 										type={file.type}
 										size={file?.size}
-										colorClassName="bg-white dark:bg-gray-850 "
+										small={true}
 									/>
 								{/if}
 							</div>

+ 0 - 20
src/lib/components/chat/MessageInput.svelte

@@ -51,7 +51,6 @@
 	import InputMenu from './MessageInput/InputMenu.svelte';
 	import VoiceRecording from './MessageInput/VoiceRecording.svelte';
 	import FilesOverlay from './MessageInput/FilesOverlay.svelte';
-	import Commands from './MessageInput/Commands.svelte';
 	import ToolServersModal from './ToolServersModal.svelte';
 
 	import RichTextInput from '../common/RichTextInput.svelte';
@@ -77,7 +76,6 @@
 	import { KokoroWorker } from '$lib/workers/KokoroWorker';
 
 	import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
-	import MentionList from '../common/RichTextInput/MentionList.svelte';
 	import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte';
 
 	const i18n = getContext('i18n');
@@ -298,16 +296,6 @@
 	};
 
 	const getCommand = () => {
-		const getWordAtCursor = (text, cursor) => {
-			if (typeof text !== 'string' || cursor == null) return '';
-			const left = text.slice(0, cursor);
-			const right = text.slice(cursor);
-			const leftWord = left.match(/(?:^|\s)([^\s]*)$/)?.[1] || '';
-
-			const rightWord = right.match(/^([^\s]*)/)?.[1] || '';
-			return leftWord + rightWord;
-		};
-
 		const chatInput = document.getElementById('chat-input');
 		let word = '';
 
@@ -319,14 +307,6 @@
 	};
 
 	const replaceCommandWithText = (text) => {
-		const getWordBoundsAtCursor = (text, cursor) => {
-			let start = cursor,
-				end = cursor;
-			while (start > 0 && !/\s/.test(text[start - 1])) --start;
-			while (end < text.length && !/\s/.test(text[end])) ++end;
-			return { start, end };
-		};
-
 		const chatInput = document.getElementById('chat-input');
 		if (!chatInput) return;
 

+ 0 - 129
src/lib/components/chat/MessageInput/Commands.svelte

@@ -1,129 +0,0 @@
-<script>
-	import { knowledge, prompts } from '$lib/stores';
-
-	import { removeLastWordFromString } from '$lib/utils';
-	import { getPrompts } from '$lib/apis/prompts';
-	import { getKnowledgeBases } from '$lib/apis/knowledge';
-
-	import Prompts from './Commands/Prompts.svelte';
-	import Knowledge from './Commands/Knowledge.svelte';
-	import Models from './Commands/Models.svelte';
-	import Spinner from '$lib/components/common/Spinner.svelte';
-
-	export let show = false;
-
-	export let files = [];
-	export let command = '';
-
-	export let onSelect = (e) => {};
-	export let onUpload = (e) => {};
-
-	export let insertTextHandler = (text) => {};
-
-	let loading = false;
-	let commandElement = null;
-
-	export const selectUp = () => {
-		commandElement?.selectUp();
-	};
-
-	export const selectDown = () => {
-		commandElement?.selectDown();
-	};
-
-	$: if (show) {
-		init();
-	}
-
-	const init = async () => {
-		loading = true;
-		await Promise.all([
-			(async () => {
-				prompts.set(await getPrompts(localStorage.token));
-			})(),
-			(async () => {
-				knowledge.set(await getKnowledgeBases(localStorage.token));
-			})()
-		]);
-		loading = false;
-	};
-</script>
-
-{#if show}
-	{#if !loading}
-		{#if command?.charAt(0) === '/'}
-			<Prompts
-				bind:this={commandElement}
-				{command}
-				onSelect={(e) => {
-					const { type, data } = e;
-
-					if (type === 'prompt') {
-						insertTextHandler(data.content);
-					}
-				}}
-			/>
-		{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
-			<Knowledge
-				bind:this={commandElement}
-				command={command.includes('\\#') ? command.slice(2) : command}
-				onSelect={(e) => {
-					const { type, data } = e;
-
-					if (type === 'knowledge') {
-						insertTextHandler('');
-
-						onUpload({
-							type: 'file',
-							data: data
-						});
-					} else if (type === 'youtube') {
-						insertTextHandler('');
-
-						onUpload({
-							type: 'youtube',
-							data: data
-						});
-					} else if (type === 'web') {
-						insertTextHandler('');
-
-						onUpload({
-							type: 'web',
-							data: data
-						});
-					}
-				}}
-			/>
-		{:else if command?.charAt(0) === '@'}
-			<Models
-				bind:this={commandElement}
-				{command}
-				onSelect={(e) => {
-					const { type, data } = e;
-
-					if (type === 'model') {
-						insertTextHandler('');
-
-						onSelect({
-							type: 'model',
-							data: data
-						});
-					}
-				}}
-			/>
-		{/if}
-	{:else}
-		<div
-			id="commands-container"
-			class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
-		>
-			<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
-				<div
-					class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
-				>
-					<Spinner />
-				</div>
-			</div>
-		</div>
-	{/if}
-{/if}

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

@@ -142,7 +142,7 @@
 	import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
 	import { all, createLowlight } from 'lowlight';
 
-	import MentionList from './RichTextInput/MentionList.svelte';
+	import MentionList from '../channel/MessageInput/MentionList.svelte';
 	import { getSuggestionRenderer } from './RichTextInput/suggestions.js';
 
 	export let oncompositionstart = (e) => {};
@@ -1369,7 +1369,7 @@
 	};
 </script>
 
-{#if showFormattingToolbar}
+{#if richText && showFormattingToolbar}
 	<div bind:this={bubbleMenuElement} id="bubble-menu" class="p-0">
 		<FormattingButtons {editor} />
 	</div>

+ 0 - 85
src/lib/components/common/RichTextInput/MentionList.svelte

@@ -1,85 +0,0 @@
-<script lang="ts">
-	export let query = '';
-
-	export let command: (payload: { id: string; label: string }) => void;
-	export let selectedIndex = 0;
-
-	let ITEMS = [
-		{ id: '1', label: 'alice' },
-		{ id: '2', label: 'alex' },
-		{ id: '3', label: 'bob' },
-		{ id: '4', label: 'charlie' },
-		{ id: '5', label: 'diana' },
-		{ id: '6', label: 'eve' },
-		{ id: '7', label: 'frank' },
-		{ id: '8', label: 'grace' },
-		{ id: '9', label: 'heidi' },
-		{ id: '10', label: 'ivan' },
-		{ id: '11', label: 'judy' },
-		{ id: '12', label: 'mallory' },
-		{ id: '13', label: 'oscar' },
-		{ id: '14', label: 'peggy' },
-		{ id: '15', label: 'trent' },
-		{ id: '16', label: 'victor' },
-		{ id: '17', label: 'walter' }
-	];
-
-	let items = ITEMS;
-
-	$: items = ITEMS.filter((u) => u.label.toLowerCase().includes(query.toLowerCase())).slice(0, 5);
-
-	const select = (index: number) => {
-		const item = items[index];
-		if (item) command(item);
-	};
-
-	const onKeyDown = (event: KeyboardEvent) => {
-		if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
-
-		if (event.key === 'ArrowUp') {
-			selectedIndex = (selectedIndex + items.length - 1) % items.length;
-			return true;
-		}
-		if (event.key === 'ArrowDown') {
-			selectedIndex = (selectedIndex + 1) % items.length;
-			return true;
-		}
-		if (event.key === 'Enter' || event.key === 'Tab') {
-			select(selectedIndex);
-			return true;
-		}
-		if (event.key === 'Escape') {
-			// tell tiptap we handled it (it will close)
-			return true;
-		}
-		return false;
-	};
-
-	// This method will be called from the suggestion renderer
-	// @ts-ignore
-	export function _onKeyDown(event: KeyboardEvent) {
-		return onKeyDown(event);
-	}
-</script>
-
-<div
-	class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 overflow-y-auto scrollbar-thin max-h-60 w-52"
-	id="suggestions-container"
->
-	{#if items.length === 0}
-		<div class=" p-4 text-gray-400">No results</div>
-	{:else}
-		{#each items as item, i}
-			<button
-				type="button"
-				on:click={() => select(i)}
-				class=" text-left w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition px-3 py-1 {i ===
-				selectedIndex
-					? 'bg-gray-50 dark:bg-gray-800 font-medium'
-					: ''}"
-			>
-				@{item.label}
-			</button>
-		{/each}
-	{/if}
-</div>

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

@@ -327,7 +327,7 @@ Based on the user's instruction, update and enhance the existing notes or select
 	});
 </script>
 
-<div class="flex items-center mb-1.5 pt-1.5">
+<div class="flex items-center mb-1.5 pt-1.5 pl-1.5 pr-2.5">
 	<div class=" -translate-x-1.5 flex items-center">
 		<button
 			class="p-0.5 bg-transparent transition rounded-lg"
@@ -358,7 +358,7 @@ Based on the user's instruction, update and enhance the existing notes or select
 	</div>
 </div>
 
-<div class="flex flex-col items-center mb-2 flex-1 @container">
+<div class="flex flex-col items-center flex-1 @container">
 	<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
 		<div class="mx-auto w-full md:px-0 h-full relative">
 			<div class=" flex flex-col h-full">
@@ -375,7 +375,7 @@ Based on the user's instruction, update and enhance the existing notes or select
 					</div>
 				</div>
 
-				<div class=" pb-2">
+				<div class=" pb-[1rem] pl-1.5 pr-2.5">
 					{#if selectedContent}
 						<div class="text-xs rounded-xl px-3.5 py-3 w-full markdown-prose-xs">
 							<blockquote>

+ 2 - 2
src/lib/components/notes/NoteEditor/Controls.svelte

@@ -17,7 +17,7 @@
 	};
 </script>
 
-<div class="flex items-center mb-1.5 pt-1.5">
+<div class="flex items-center mb-1.5 pt-1.5 pl-1.5 pr-2.5">
 	<div class=" -translate-x-1.5 flex items-center">
 		<button
 			class="p-0.5 bg-transparent transition rounded-lg"
@@ -36,7 +36,7 @@
 	</div>
 </div>
 
-<div class="mt-1">
+<div class="mt-1 pl-1.5 pr-2.5">
 	<div class="pb-10">
 		{#if files.length > 0}
 			<div class=" text-xs font-medium pb-1">{$i18n.t('Files')}</div>

+ 1 - 1
src/lib/components/notes/NotePanel.svelte

@@ -98,7 +98,7 @@
 		{#if show}
 			<div class="flex max-h-full min-h-full">
 				<div
-					class="w-full pl-1.5 pr-2.5 pt-2 bg-white dark:shadow-lg dark:bg-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
+					class="w-full pt-2 bg-white dark:shadow-lg dark:bg-gray-850 z-40 pointer-events-auto overflow-y-auto scrollbar-hidden flex flex-col"
 				>
 					<slot />
 				</div>