feat: ai autocompletion

This commit is contained in:
Timothy Jaeryang Baek 2024-11-28 23:22:53 -08:00
parent 8300fa85b0
commit 95000c7b15
3 changed files with 126 additions and 3 deletions

View File

@ -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;

View File

@ -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>

View File

@ -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
},
},
}),
]
},
})