Bladeren bron

feat: ai autocompletion

Timothy Jaeryang Baek 5 maanden geleden
bovenliggende
commit
95000c7b15

+ 7 - 0
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;

+ 23 - 3
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 @@
 </script>
 
 <div bind:this={element} class="relative w-full min-w-full h-full min-h-fit {className}" />
+
+<style>
+	.ai-autocompletion::after {
+		content: attr(data-suggestion);
+		color: var(--gray-5);
+		pointer-events: none;
+	}
+</style>

+ 96 - 0
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
+          },
+        },
+      }),
+    ]
+  },
+})