diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte
index 0848a19dd5..dd8006b895 100644
--- a/src/lib/components/chat/Chat.svelte
+++ b/src/lib/components/chat/Chat.svelte
@@ -96,6 +96,8 @@
let controlPane;
let controlPaneComponent;
+ let messageInput;
+
let autoScroll = true;
let processing = '';
let messagesContainerElement: HTMLDivElement;
@@ -140,24 +142,39 @@
let params = {};
$: if (chatIdProp) {
- (async () => {
- loading = true;
+ navigateHandler();
+ }
- prompt = '';
- files = [];
- selectedToolIds = [];
- selectedFilterIds = [];
- webSearchEnabled = false;
- imageGenerationEnabled = false;
+ const navigateHandler = async () => {
+ loading = true;
- if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
+ prompt = '';
+ messageInput?.setText('');
+
+ files = [];
+ selectedToolIds = [];
+ selectedFilterIds = [];
+ webSearchEnabled = false;
+ imageGenerationEnabled = false;
+
+ const storageChatInput = sessionStorage.getItem(
+ `chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
+ );
+
+ if (chatIdProp && (await loadChat())) {
+ await tick();
+ loading = false;
+ window.setTimeout(() => scrollToBottom(), 0);
+
+ await tick();
+
+ if (storageChatInput) {
try {
- const input = JSON.parse(
- sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
- );
+ const input = JSON.parse(storageChatInput);
+ console.log(input);
if (!$temporaryChatEnabled) {
- prompt = input.prompt;
+ messageInput?.setText(input.prompt);
files = input.files;
selectedToolIds = input.selectedToolIds;
selectedFilterIds = input.selectedFilterIds;
@@ -168,17 +185,12 @@
} catch (e) {}
}
- if (chatIdProp && (await loadChat())) {
- await tick();
- loading = false;
- window.setTimeout(() => scrollToBottom(), 0);
- const chatInput = document.getElementById('chat-input');
- chatInput?.focus();
- } else {
- await goto('/');
- }
- })();
- }
+ const chatInput = document.getElementById('chat-input');
+ chatInput?.focus();
+ } else {
+ await goto('/');
+ }
+ };
$: if (selectedModels && chatIdProp !== '') {
saveSessionSelectedModels();
@@ -405,7 +417,7 @@
const inputElement = document.getElementById('chat-input');
if (inputElement) {
- prompt = event.data.text;
+ messageInput?.setText(event.data.text);
inputElement.focus();
}
}
@@ -443,8 +455,19 @@
}
});
- if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
+ const storageChatInput = sessionStorage.getItem(
+ `chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
+ );
+
+ if (!chatIdProp) {
+ loading = false;
+ await tick();
+ }
+
+ if (storageChatInput) {
prompt = '';
+ messageInput?.setText('');
+
files = [];
selectedToolIds = [];
selectedFilterIds = [];
@@ -453,12 +476,11 @@
codeInterpreterEnabled = false;
try {
- const input = JSON.parse(
- sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
- );
+ const input = JSON.parse(storageChatInput);
+ console.log(input);
if (!$temporaryChatEnabled) {
- prompt = input.prompt;
+ messageInput?.setText(input.prompt);
files = input.files;
selectedToolIds = input.selectedToolIds;
selectedFilterIds = input.selectedFilterIds;
@@ -469,11 +491,6 @@
} catch (e) {}
}
- if (!chatIdProp) {
- loading = false;
- await tick();
- }
-
showControls.subscribe(async (value) => {
if (controlPane && !$mobile) {
try {
@@ -833,12 +850,13 @@
}
if ($page.url.searchParams.get('q')) {
- prompt = $page.url.searchParams.get('q') ?? '';
+ const q = $page.url.searchParams.get('q') ?? '';
+ messageInput?.setText(q);
- if (prompt) {
+ if (q) {
if (($page.url.searchParams.get('submit') ?? 'true') === 'true') {
await tick();
- submitPrompt(prompt);
+ submitPrompt(q);
}
}
}
@@ -1071,7 +1089,7 @@
};
const createMessagePair = async (userPrompt) => {
- prompt = '';
+ messageInput?.setText('');
if (selectedModels.length === 0) {
toast.error($i18n.t('Model not selected'));
} else {
@@ -1392,7 +1410,7 @@
return;
}
- prompt = '';
+ messageInput?.setText('');
// Reset chat input textarea
if (!($settings?.richTextInput ?? true)) {
@@ -1413,7 +1431,7 @@
);
files = [];
- prompt = '';
+ messageInput?.setText('');
// Create user message
let userMessageId = uuidv4();
@@ -2104,6 +2122,7 @@
{
+ const chatInput = document.getElementById('chat-input');
+
+ if (chatInput) {
+ if ($settings?.richTextInput ?? true) {
+ chatInputElement.setText(text);
+ } else {
+ // chatInput.value = text;
+ prompt = text;
+ }
+ }
+ };
+
+ function 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 getCommand = () => {
+ const chatInput = document.getElementById('chat-input');
+ let word = '';
+
+ if (chatInput) {
+ if ($settings?.richTextInput ?? true) {
+ word = chatInputElement?.getWordAtDocPos();
+ } else {
+ const cursor = chatInput ? chatInput.selectionStart : prompt.length;
+ word = getWordAtCursor(prompt, cursor);
+ }
+ }
+
+ return word;
+ };
+
+ function 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 };
+ }
+
+ function replaceCommandWithText(text) {
+ const chatInput = document.getElementById('chat-input');
+ if (!chatInput) return;
+
+ if ($settings?.richTextInput ?? true) {
+ chatInputElement?.replaceCommandWithText(text);
+ } else {
+ const cursor = chatInput.selectionStart;
+ const { start, end } = getWordBoundsAtCursor(prompt, cursor);
+ prompt = prompt.slice(0, start) + text + prompt.slice(end);
+ chatInput.focus();
+ chatInput.setSelectionRange(start + text.length, start + text.length);
+ }
+ }
+
+ const inputVariableHandler = async (text: string) => {
+ return text;
+ };
+
+ const textVariableHandler = async (text: string) => {
+ if (text.includes('{{CLIPBOARD}}')) {
+ const clipboardText = await navigator.clipboard.readText().catch((err) => {
+ toast.error($i18n.t('Failed to read clipboard contents'));
+ return '{{CLIPBOARD}}';
+ });
+
+ const clipboardItems = await navigator.clipboard.read();
+
+ let imageUrl = null;
+ for (const item of clipboardItems) {
+ // Check for known image types
+ for (const type of item.types) {
+ if (type.startsWith('image/')) {
+ const blob = await item.getType(type);
+ imageUrl = URL.createObjectURL(blob);
+ }
+ }
+ }
+
+ if (imageUrl) {
+ files = [
+ ...files,
+ {
+ type: 'image',
+ url: imageUrl
+ }
+ ];
+ }
+
+ text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
+ }
+
+ if (text.includes('{{USER_LOCATION}}')) {
+ let location;
+ try {
+ location = await getUserPosition();
+ } catch (error) {
+ toast.error($i18n.t('Location access not allowed'));
+ location = 'LOCATION_UNKNOWN';
+ }
+ text = text.replaceAll('{{USER_LOCATION}}', String(location));
+ }
+
+ if (text.includes('{{USER_NAME}}')) {
+ const name = $_user?.name || 'User';
+ text = text.replaceAll('{{USER_NAME}}', name);
+ }
+
+ if (text.includes('{{USER_LANGUAGE}}')) {
+ const language = localStorage.getItem('locale') || 'en-US';
+ text = text.replaceAll('{{USER_LANGUAGE}}', language);
+ }
+
+ if (text.includes('{{CURRENT_DATE}}')) {
+ const date = getFormattedDate();
+ text = text.replaceAll('{{CURRENT_DATE}}', date);
+ }
+
+ if (text.includes('{{CURRENT_TIME}}')) {
+ const time = getFormattedTime();
+ text = text.replaceAll('{{CURRENT_TIME}}', time);
+ }
+
+ if (text.includes('{{CURRENT_DATETIME}}')) {
+ const dateTime = getCurrentDateTime();
+ text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
+ }
+
+ if (text.includes('{{CURRENT_TIMEZONE}}')) {
+ const timezone = getUserTimezone();
+ text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
+ }
+
+ if (text.includes('{{CURRENT_WEEKDAY}}')) {
+ const weekday = getWeekday();
+ text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
+ }
+
+ text = await inputVariableHandler(text);
+ return text;
+ };
+
+ const insertTextAtCursor = async (text: string) => {
+ const chatInput = document.getElementById('chat-input');
+ if (!chatInput) return;
+
+ text = await textVariableHandler(text);
+ if (command) {
+ replaceCommandWithText(text);
+ } else {
+ if ($settings?.richTextInput ?? true) {
+ 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);
+ }
+ } else {
+ const cursor = chatInput.selectionStart;
+ prompt = prompt.slice(0, cursor) + text + prompt.slice(cursor);
+ chatInput.focus();
+ chatInput.setSelectionRange(cursor + text.length, cursor + text.length);
+ }
+ }
+
+ await tick();
+ const chatInputContainer = document.getElementById('chat-input-container');
+ if (chatInputContainer) {
+ chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
+ }
+
+ await tick();
+ if (chatInput) {
+ chatInput.focus();
+ chatInput.dispatchEvent(new Event('input'));
+
+ const words = extractCurlyBraceWords(prompt);
+
+ if (words.length > 0) {
+ const word = words.at(0);
+ await tick();
+
+ if (!($settings?.richTextInput ?? true)) {
+ // Move scroll to the first word
+ chatInput.setSelectionRange(word.startIndex, word.endIndex + 1);
+ chatInput.focus();
+
+ const selectionRow =
+ (word?.startIndex - (word?.startIndex % chatInput.cols)) / chatInput.cols;
+ const lineHeight = chatInput.clientHeight / chatInput.rows;
+
+ chatInput.scrollTop = lineHeight * selectionRow;
+ }
+ } else {
+ chatInput.scrollTop = chatInput.scrollHeight;
+ }
+ }
+ };
+
+ let command = '';
+
+ let showCommands = false;
+ $: showCommands = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
+
let showTools = false;
let loaded = false;
@@ -583,20 +802,36 @@
{
- dispatch('upload', e.detail);
- }}
- on:select={(e) => {
- const data = e.detail;
+ show={showCommands}
+ {command}
+ insertTextHandler={insertTextAtCursor}
+ onUpload={(e) => {
+ const { type, data } = e;
- if (data?.type === 'model') {
- atSelectedModel = data.data;
+ if (type === 'file') {
+ if (files.find((f) => f.id === data.id)) {
+ return;
+ }
+ files = [
+ ...files,
+ {
+ ...data,
+ status: 'processed'
+ }
+ ];
+ } else {
+ dispatch('upload', e);
+ }
+ }}
+ onSelect={(e) => {
+ const { type, data } = e;
+
+ if (type === 'model') {
+ atSelectedModel = data;
}
- const chatInputElement = document.getElementById('chat-input');
- chatInputElement?.focus();
+ document.getElementById('chat-input')?.focus();
}}
/>
@@ -770,8 +1005,12 @@
>
{
+ prompt = e.md;
+ command = getCommand();
+ }}
+ json={true}
messageInput={true}
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
(!$mobile ||
@@ -990,6 +1229,12 @@
class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-3 px-1 resize-none"
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
bind:value={prompt}
+ on:input={() => {
+ command = getCommand();
+ }}
+ on:click={() => {
+ command = getCommand();
+ }}
on:compositionstart={() => (isComposing = true)}
on:compositionend={() => (isComposing = false)}
on:keydown={async (e) => {
@@ -1137,17 +1382,20 @@
if (words.length > 0) {
const word = words.at(0);
- const fullPrompt = prompt;
- prompt = prompt.substring(0, word?.endIndex + 1);
- await tick();
+ if (word && e.target instanceof HTMLTextAreaElement) {
+ // Prevent default tab behavior
+ e.preventDefault();
+ e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
+ e.target.focus();
- e.target.scrollTop = e.target.scrollHeight;
- prompt = fullPrompt;
- await tick();
+ const selectionRow =
+ (word?.startIndex - (word?.startIndex % e.target.cols)) /
+ e.target.cols;
+ const lineHeight = e.target.clientHeight / e.target.rows;
- e.preventDefault();
- e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
+ e.target.scrollTop = lineHeight * selectionRow;
+ }
}
e.target.style.height = '';
diff --git a/src/lib/components/chat/MessageInput/Commands.svelte b/src/lib/components/chat/MessageInput/Commands.svelte
index bcea28d1e7..af71458522 100644
--- a/src/lib/components/chat/MessageInput/Commands.svelte
+++ b/src/lib/components/chat/MessageInput/Commands.svelte
@@ -1,9 +1,4 @@
-{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
+{#if filteredItems.length > 0 || command?.substring(1).startsWith('http')}
{
console.log(item);
- confirmSelect(item);
+ confirmSelect('knowledge', item);
}}
on:mousemove={() => {
selectedIdx = idx;
@@ -298,18 +272,15 @@
-->
{/each}
- {#if prompt
- .split(' ')
- .some((s) => s.substring(1).startsWith('https://www.youtube.com') || s
- .substring(1)
- .startsWith('https://youtu.be'))}
+ {#if command.substring(1).startsWith('https://www.youtube.com') || command
+ .substring(1)
+ .startsWith('https://youtu.be')}
- {:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
+ {:else if command.substring(1).startsWith('http')}