diff --git a/src/app.css b/src/app.css index 659498add..08da4501d 100644 --- a/src/app.css +++ b/src/app.css @@ -214,6 +214,13 @@ input[type='number'] { height: 0; } +.ai-autocompletion::after { + color: #a0a0a0; + + content: attr(data-suggestion); + pointer-events: none; + } + .tiptap > pre > code { border-radius: 0.4rem; font-size: 0.85rem; diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index fc24ab063..a0420d447 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -10,16 +10,18 @@ import { createEventDispatcher } from 'svelte'; const eventDispatch = createEventDispatcher(); - import { EditorState, Plugin, TextSelection } from 'prosemirror-state'; + import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state'; + import { Decoration, DecorationSet } from 'prosemirror-view'; import { Editor } from '@tiptap/core'; + import { AIAutocompletion } from './RichTextInput/AutoCompletion.js'; + import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; import Placeholder from '@tiptap/extension-placeholder'; import Highlight from '@tiptap/extension-highlight'; import Typography from '@tiptap/extension-typography'; import StarterKit from '@tiptap/starter-kit'; - import { all, createLowlight } from 'lowlight'; import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants'; @@ -32,6 +34,7 @@ export let value = ''; export let id = ''; + export let autocomplete = false; export let messageInput = false; export let shiftEnter = false; export let largeTextAsFile = false; @@ -147,7 +150,16 @@ }), Highlight, Typography, - Placeholder.configure({ placeholder }) + Placeholder.configure({ placeholder }), + AIAutocompletion.configure({ + generateCompletion: async (text) => { + // Implement your AI text generation logic here + // This should return a Promise that resolves to the suggested text + + console.log(text); + return 'AI-generated suggestion'; + } + }) ], content: content, autofocus: true, @@ -292,3 +304,11 @@
+ + diff --git a/src/lib/components/common/RichTextInput/AutoCompletion.js b/src/lib/components/common/RichTextInput/AutoCompletion.js new file mode 100644 index 000000000..fbd6e18fd --- /dev/null +++ b/src/lib/components/common/RichTextInput/AutoCompletion.js @@ -0,0 +1,96 @@ +import { Extension } from '@tiptap/core' +import { Plugin, PluginKey } from 'prosemirror-state' + +export const AIAutocompletion = Extension.create({ + name: 'aiAutocompletion', + + addOptions() { + return { + generateCompletion: () => Promise.resolve(''), + } + }, + + addGlobalAttributes() { + return [ + { + types: ['paragraph'], + attributes: { + class: { + default: null, + parseHTML: element => element.getAttribute('class'), + renderHTML: attributes => { + if (!attributes.class) return {} + return { class: attributes.class } + }, + }, + 'data-prompt': { + default: null, + parseHTML: element => element.getAttribute('data-prompt'), + renderHTML: attributes => { + if (!attributes['data-prompt']) return {} + return { 'data-prompt': attributes['data-prompt'] } + }, + }, + 'data-suggestion': { + default: null, + parseHTML: element => element.getAttribute('data-suggestion'), + renderHTML: attributes => { + if (!attributes['data-suggestion']) return {} + return { 'data-suggestion': attributes['data-suggestion'] } + }, + }, + }, + }, + ] + }, + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('aiAutocompletion'), + props: { + handleKeyDown: (view, event) => { + if (event.key !== 'Tab') return false + + const { state, dispatch } = view + const { selection } = state + const { $head } = selection + + if ($head.parent.type.name !== 'paragraph') return false + + const node = $head.parent + const prompt = node.textContent + + if (!node.attrs['data-suggestion']) { + // Generate completion + this.options.generateCompletion(prompt).then(suggestion => { + if (suggestion && suggestion.trim() !== '') { + dispatch(state.tr.setNodeMarkup($head.before(), null, { + ...node.attrs, + class: 'ai-autocompletion', + 'data-prompt': prompt, + 'data-suggestion': suggestion, + })) + } + }) + } else { + // Accept suggestion + const suggestion = node.attrs['data-suggestion'] + dispatch(state.tr + .insertText(suggestion, $head.pos) + .setNodeMarkup($head.before(), null, { + ...node.attrs, + class: null, + 'data-prompt': null, + 'data-suggestion': null, + }) + ) + } + + return true + }, + }, + }), + ] + }, +}) \ No newline at end of file