diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index c7ca93b902..f9fbc32f6c 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/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) => void; let inputVariables: Record = {}; let inputVariableValues = {}; - const inputVariableHandler = async (text: string) => { + const inputVariableHandler = async (text: string): Promise => { 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((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) => { - 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'); - text = await textVariableHandler(text || ''); + if (chatInput) { + 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 @@ }); - +{#if loaded} + -{#if acceptFiles} - { - if (inputFiles && inputFiles.length > 0) { - inputFilesHandler(Array.from(inputFiles)); - } else { - toast.error($i18n.t(`File not found.`)); - } + {#if acceptFiles} + { + if (inputFiles && inputFiles.length > 0) { + inputFilesHandler(Array.from(inputFiles)); + } else { + toast.error($i18n.t(`File not found.`)); + } - filesInputElement.value = ''; - }} + filesInputElement.value = ''; + }} + /> + {/if} + + -{/if} - { - inputVariableValues = { ...inputVariableValues, ...variableValues }; - replaceVariables(inputVariableValues); - }} -/> - -
-
-
-
-
- {#if scrollEnd === false} -
- -
- {/if} -
- -
-
- {#if typingUsers.length > 0} -
- - {typingUsers.map((user) => user.name).join(', ')} - - {$i18n.t('is typing...')} + + + +
{/if}
- +
+
+ {#if typingUsers.length > 0} +
+ + {typingUsers.map((user) => user.name).join(', ')} + + {$i18n.t('is typing...')} +
+ {/if} +
+
-
-
- {#if recording} - { - recording = false; +
+ {#if recording} + { + 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} -
{ - submitHandler(); - }} - > -
+ {:else} + { + submitHandler(); + }} > - {#if files.length > 0} -
- {#each files as file, fileIdx} - {#if file.type === 'image'} -
-
- input +
+ {#if files.length > 0} +
+ {#each files as file, fileIdx} + {#if file.type === 'image'} +
+
+ +
+
+ +
-
+ {:else} + { + files.splice(fileIdx, 1); + files = files; + }} + on:click={() => { + console.log(file); + }} + /> + {/if} + {/each} +
+ {/if} + +
+
+ {#key $settings?.richTextInput} + 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} +
+
+ +
+
+ + {#if acceptFiles} + { + filesInputElement.click(); + }} + > -
-
- {:else} - { - files.splice(fileIdx, 1); - files = files; - }} - on:click={() => { - console.log(file); - }} - /> - {/if} - {/each} -
- {/if} + + {/if} + +
-
-
- 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); - }} - /> -
-
- -
-
- - {#if acceptFiles} - { - filesInputElement.click(); - }} - > +
+ {#if content === ''} + - + {/if} - -
-
- {#if content === ''} - - - - {/if} - -
- {#if inputLoading && onStop} -
- - - -
- {:else} -
- - + +
+ {:else} +
+ + - -
- {/if} + + + + + +
+ {/if} +
-
- - {/if} + + {/if} +
-
+{/if} diff --git a/src/lib/components/channel/MessageInput/InputMenu.svelte b/src/lib/components/channel/MessageInput/InputMenu.svelte index 7226c34cb9..902a49f7e0 100644 --- a/src/lib/components/channel/MessageInput/InputMenu.svelte +++ b/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 @@
- {#if !$mobile} - { - screenCaptureHandler(); - }} - > - -
{$i18n.t('Capture')}
-
- {/if} - { uploadFilesHandler(); }} > - +
{$i18n.t('Upload Files')}
+ + {#if !$mobile} + { + screenCaptureHandler(); + }} + > + +
{$i18n.t('Capture')}
+
+ {/if}
diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte new file mode 100644 index 0000000000..d84c89e6cb --- /dev/null +++ b/src/lib/components/channel/MessageInput/MentionList.svelte @@ -0,0 +1,81 @@ + + +{#if filteredItems.length} +
+
+
+ {$i18n.t('Models')} +
+ {#each filteredItems as item, i} + + {/each} +
+
+{/if} diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index 649529a6f9..134a83a128 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -138,9 +138,7 @@ id="message-{message.id}" dir={$settings.chatDirection} > -
+
{#if showUserProfile} {/if}
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index cd8ee35426..adb347a070 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/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; diff --git a/src/lib/components/chat/MessageInput/Commands.svelte b/src/lib/components/chat/MessageInput/Commands.svelte deleted file mode 100644 index af71458522..0000000000 --- a/src/lib/components/chat/MessageInput/Commands.svelte +++ /dev/null @@ -1,129 +0,0 @@ - - -{#if show} - {#if !loading} - {#if command?.charAt(0) === '/'} - { - 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('# '))} - { - 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) === '@'} - { - const { type, data } = e; - - if (type === 'model') { - insertTextHandler(''); - - onSelect({ - type: 'model', - data: data - }); - } - }} - /> - {/if} - {:else} -
-
-
- -
-
-
- {/if} -{/if} diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index 2e649abd30..caa62bb454 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/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 @@ }; -{#if showFormattingToolbar} +{#if richText && showFormattingToolbar}
diff --git a/src/lib/components/common/RichTextInput/MentionList.svelte b/src/lib/components/common/RichTextInput/MentionList.svelte deleted file mode 100644 index 85f713bd57..0000000000 --- a/src/lib/components/common/RichTextInput/MentionList.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - -
- {#if items.length === 0} -
No results
- {:else} - {#each items as item, i} - - {/each} - {/if} -
diff --git a/src/lib/components/notes/NoteEditor/Chat.svelte b/src/lib/components/notes/NoteEditor/Chat.svelte index 9a509a3dc9..082b70baf5 100644 --- a/src/lib/components/notes/NoteEditor/Chat.svelte +++ b/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 }); -
+
-
+
@@ -375,7 +375,7 @@ Based on the user's instruction, update and enhance the existing notes or select
-
+
{#if selectedContent}
diff --git a/src/lib/components/notes/NoteEditor/Controls.svelte b/src/lib/components/notes/NoteEditor/Controls.svelte index 6ac64ba3ab..091c347264 100644 --- a/src/lib/components/notes/NoteEditor/Controls.svelte +++ b/src/lib/components/notes/NoteEditor/Controls.svelte @@ -17,7 +17,7 @@ }; -
+
-
+
{#if files.length > 0}
{$i18n.t('Files')}
diff --git a/src/lib/components/notes/NotePanel.svelte b/src/lib/components/notes/NotePanel.svelte index d26f4b72c7..ec5f62675e 100644 --- a/src/lib/components/notes/NotePanel.svelte +++ b/src/lib/components/notes/NotePanel.svelte @@ -98,7 +98,7 @@ {#if show}