diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index ce298ef837..b854e342dd 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -143,7 +143,7 @@ import { Decoration, DecorationSet } from 'prosemirror-view'; import { Editor, Extension, markInputRule, mergeAttributes } from '@tiptap/core'; - import { AIAutocompletion } from './RichTextInput/AutoCompletion.js'; + import { AIAutocompletion, setFollowUpSuggestion } from './RichTextInput/AutoCompletion.js'; import StarterKit from '@tiptap/starter-kit'; @@ -322,23 +322,7 @@ export let followUpSuggestion = ''; $: if (editor && !editor.isDestroyed) { - const { doc } = editor.state; - const node = doc.firstChild; - if (node?.type.name === 'paragraph' && !node.attrs['data-prompt']) { - const suggestion = doc.childCount === 1 && node.content.size === 0 ? followUpSuggestion : ''; - if ((node.attrs['data-suggestion'] ?? '') !== suggestion) { - editor.view.dispatch( - editor.state.tr - .setNodeMarkup(0, null, { - ...node.attrs, - class: suggestion ? 'ai-autocompletion' : null, - 'data-prompt': suggestion ? '' : null, - 'data-suggestion': suggestion || null - }) - .setMeta('addToHistory', false) - ); - } - } + setFollowUpSuggestion(editor.view, followUpSuggestion); } export let messageInput = false; diff --git a/src/lib/components/common/RichTextInput/AutoCompletion.js b/src/lib/components/common/RichTextInput/AutoCompletion.js index 89cff891ea..025ee554cc 100644 --- a/src/lib/components/common/RichTextInput/AutoCompletion.js +++ b/src/lib/components/common/RichTextInput/AutoCompletion.js @@ -13,6 +13,31 @@ Intelligently reset suggestions on new input. import { Extension } from '@tiptap/core'; import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; + +const aiAutocompletionKey = new PluginKey('aiAutocompletion'); + +const getFollowUpSuggestion = (state) => { + const { doc } = state; + const isEmpty = + doc.childCount === 1 && + doc.firstChild.type.name === 'paragraph' && + doc.firstChild.content.size === 0; + return isEmpty ? aiAutocompletionKey.getState(state) : ''; +}; + +export const setFollowUpSuggestion = (view, suggestion) => { + const current = aiAutocompletionKey.getState(view.state); + if (current === undefined || current === suggestion) return; + view.dispatch(view.state.tr.setMeta(aiAutocompletionKey, suggestion)); +}; + +const acceptFollowUpSuggestion = (view) => { + const suggestion = getFollowUpSuggestion(view.state); + if (!suggestion) return false; + view.dispatch(view.state.tr.insertText(suggestion)); + return true; +}; export const AIAutocompletion = Extension.create({ name: 'aiAutocompletion', @@ -131,8 +156,23 @@ export const AIAutocompletion = Extension.create({ return [ new Plugin({ - key: new PluginKey('aiAutocompletion'), + key: aiAutocompletionKey, + state: { + init: () => '', + apply: (tr, suggestion) => tr.getMeta(aiAutocompletionKey) ?? suggestion + }, props: { + // A decoration is patched in place, so removing it cannot break an IME composition + decorations: (state) => { + const suggestion = getFollowUpSuggestion(state); + if (!suggestion) return null; + return DecorationSet.create(state.doc, [ + Decoration.node(0, state.doc.firstChild.nodeSize, { + class: 'ai-autocompletion', + 'data-suggestion': suggestion + }) + ]); + }, handleKeyDown: (view, event) => { if (isComposing || event.isComposing || (event.key === 'Tab' && event.shiftKey)) return false; @@ -145,6 +185,8 @@ export const AIAutocompletion = Extension.create({ const node = $head.parent; if (event.key === 'Tab') { + if (acceptFollowUpSuggestion(view)) return true; + // if (!node.attrs['data-suggestion']) { // // Generate completion // if (loading) return true @@ -219,6 +261,8 @@ export const AIAutocompletion = Extension.create({ // Check if the swipe was primarily horizontal and to the right if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 50) { + if (acceptFollowUpSuggestion(view)) return true; + const { state, dispatch } = view; const { selection } = state; const { $head } = selection;