Skip to content

Commit 643c739

Browse files
committed
wip
1 parent ac61814 commit 643c739

File tree

3 files changed

+68
-132
lines changed

3 files changed

+68
-132
lines changed

examples/09-ai/01-minimal/src/App.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,45 @@ import { getEnv } from "./getEnv";
2727
const BASE_URL =
2828
getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";
2929

30+
async function autoCompleteProvider(
31+
editor: BlockNoteEditor<any, any, any>,
32+
signal: AbortSignal,
33+
) {
34+
// TODO:
35+
// - API is very prosemirror-based, make something more BlockNote-native
36+
// - Add simple method to retrieve relevant context (e.g. block content / json until selection)
37+
38+
const state = editor.prosemirrorState;
39+
const text = state.doc.textBetween(
40+
state.selection.from - 300,
41+
state.selection.from,
42+
);
43+
44+
const response = await fetch(
45+
`https://localhost:3000/ai/autocomplete/generateText`,
46+
{
47+
method: "POST",
48+
body: JSON.stringify({ text }),
49+
signal,
50+
},
51+
);
52+
const data = await response.json();
53+
return data.suggestions.map((suggestion: string) => ({
54+
position: state.selection.from,
55+
suggestion: suggestion,
56+
}));
57+
// return [
58+
// {
59+
// position: state.selection.from,
60+
// suggestion: "Hello World",
61+
// },
62+
// {
63+
// position: state.selection.from,
64+
// suggestion: "Hello Planet",
65+
// },
66+
// ];
67+
}
68+
3069
export default function App() {
3170
// Creates a new editor instance.
3271
const editor = useCreateBlockNote({
@@ -42,7 +81,7 @@ export default function App() {
4281
api: `${BASE_URL}/regular/streamText`,
4382
}),
4483
}),
45-
createAIAutoCompleteExtension(),
84+
createAIAutoCompleteExtension({ autoCompleteProvider }),
4685
],
4786
// We set some initial content for demo purposes
4887
initialContent: [

packages/xl-ai-server/src/routes/autocomplete.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ autocompleteRoute.post("/generateText", async (c) => {
2424
- max 3 suggestions
2525
- keep it short, max 5 words per suggestion
2626
- don't include other text (or explanations)
27-
- ONLY return the text to be appended. Your suggestion will EXACTLY replace [SUGGESTION_HERE].
28-
- DONT include the original text / characters (prefix)
29-
- add a space (or other relevant punctuation) before the suggestion if starting a new word`,
27+
- YOU MUST ONLY return the text to be appended. Your suggestion will EXACTLY replace [SUGGESTION_HERE].
28+
- YOU MUST NOT include the original text / characters (prefix) in your suggestion.
29+
- YOU MUST add a space (or other relevant punctuation) before the suggestion IF starting a new word (the suggestion will be directly concatenated to the text)`,
3030
messages: [
3131
{
3232
role: "user",

packages/xl-ai/src/plugins/AutoCompletePlugin.ts

Lines changed: 25 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -42,39 +42,10 @@ type AutoCompleteSuggestion = {
4242
suggestion: string;
4343
};
4444

45-
async function fetchAutoCompleteSuggestions(
46-
state: EditorState,
47-
_signal: AbortSignal,
48-
) {
49-
// TODO: options to get block json until selection
50-
const text = state.doc.textBetween(
51-
state.selection.from - 300,
52-
state.selection.from,
53-
);
54-
55-
const response = await fetch(
56-
`https://localhost:3000/ai/autocomplete/generateText`,
57-
{
58-
method: "POST",
59-
body: JSON.stringify({ text }),
60-
},
61-
);
62-
const data = await response.json();
63-
return data.suggestions.map((suggestion: string) => ({
64-
position: state.selection.from,
65-
suggestion: suggestion,
66-
}));
67-
// return [
68-
// {
69-
// position: state.selection.from,
70-
// suggestion: "Hello World",
71-
// },
72-
// {
73-
// position: state.selection.from,
74-
// suggestion: "Hello Planet",
75-
// },
76-
// ];
77-
}
45+
type AutoCompleteProvider = (
46+
editor: BlockNoteEditor<any, any, any>,
47+
signal: AbortSignal,
48+
) => Promise<AutoCompleteSuggestion[]>;
7849

7950
function getMatchingSuggestions(
8051
autoCompleteSuggestions: AutoCompleteSuggestion[],
@@ -131,10 +102,10 @@ export class AutoCompleteProseMirrorPlugin<
131102
private autoCompleteSuggestions: AutoCompleteSuggestion[] = [];
132103

133104
private debounceFetchSuggestions = debounceWithAbort(
134-
async (state: EditorState, signal: AbortSignal) => {
105+
async (editor: BlockNoteEditor<any, any, any>, signal: AbortSignal) => {
135106
// fetch suggestions
136-
const autoCompleteSuggestions = await fetchAutoCompleteSuggestions(
137-
state,
107+
const autoCompleteSuggestions = await this.options.autoCompleteProvider(
108+
editor,
138109
signal,
139110
);
140111

@@ -155,7 +126,9 @@ export class AutoCompleteProseMirrorPlugin<
155126

156127
constructor(
157128
private readonly editor: BlockNoteEditor<BSchema, I, S>,
158-
options: {},
129+
private readonly options: {
130+
autoCompleteProvider: AutoCompleteProvider;
131+
},
159132
) {
160133
super();
161134

@@ -179,7 +152,7 @@ export class AutoCompleteProseMirrorPlugin<
179152
// Apply changes to the plugin state from an editor transaction.
180153
apply: (
181154
transaction,
182-
prev,
155+
_prev,
183156
_oldState,
184157
newState,
185158
): AutoCompleteState => {
@@ -204,96 +177,19 @@ export class AutoCompleteProseMirrorPlugin<
204177

205178
// No matching suggestions, if isUserInput is true, debounce fetch suggestions
206179
if (transaction.getMeta(autoCompletePluginKey)?.isUserInput) {
207-
this.debounceFetchSuggestions(newState).catch((error) => {
208-
/* eslint-disable-next-line no-console */
209-
console.error(error);
180+
// TODO: this queueMicrotask is a workaround to ensure the transaction is applied before the debounceFetchSuggestions is called
181+
// (discuss with Nick what ideal architecture would be)
182+
queueMicrotask(() => {
183+
this.debounceFetchSuggestions(self.editor).catch((error) => {
184+
/* eslint-disable-next-line no-console */
185+
console.error(error);
186+
});
210187
});
211188
} else {
212189
// clear suggestions
213190
this.autoCompleteSuggestions = [];
214191
}
215192
return undefined;
216-
217-
// Ignore transactions in code blocks.
218-
// if (transaction.selection.$from.parent.type.spec.code) {
219-
// return prev;
220-
// }
221-
222-
// // Either contains the trigger character if the menu should be shown,
223-
// // or null if it should be hidden.
224-
// const suggestionPluginTransactionMeta: {
225-
// triggerCharacter: string;
226-
// deleteTriggerCharacter?: boolean;
227-
// ignoreQueryLength?: boolean;
228-
// } | null = transaction.getMeta(autoCompletePluginKey);
229-
230-
// if (
231-
// typeof suggestionPluginTransactionMeta === "object" &&
232-
// suggestionPluginTransactionMeta !== null
233-
// ) {
234-
// if (prev) {
235-
// // Close the previous menu if it exists
236-
// this.closeMenu();
237-
// }
238-
// const trackedPosition = trackPosition(
239-
// editor,
240-
// newState.selection.from -
241-
// // Need to account for the trigger char that was inserted, so we offset the position by the length of the trigger character.
242-
// suggestionPluginTransactionMeta.triggerCharacter.length,
243-
// );
244-
// return {
245-
// triggerCharacter:
246-
// suggestionPluginTransactionMeta.triggerCharacter,
247-
// deleteTriggerCharacter:
248-
// suggestionPluginTransactionMeta.deleteTriggerCharacter !==
249-
// false,
250-
// // When reading the queryStartPos, we offset the result by the length of the trigger character, to make it easy on the caller
251-
// queryStartPos: () =>
252-
// trackedPosition() +
253-
// suggestionPluginTransactionMeta.triggerCharacter.length,
254-
// query: "",
255-
// decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
256-
// ignoreQueryLength:
257-
// suggestionPluginTransactionMeta?.ignoreQueryLength,
258-
// };
259-
// }
260-
261-
// // Checks if the menu is hidden, in which case it doesn't need to be hidden or updated.
262-
// if (prev === undefined) {
263-
// return prev;
264-
// }
265-
266-
// // Checks if the menu should be hidden.
267-
// if (
268-
// // Highlighting text should hide the menu.
269-
// newState.selection.from !== newState.selection.to ||
270-
// // Transactions with plugin metadata should hide the menu.
271-
// suggestionPluginTransactionMeta === null ||
272-
// // Certain mouse events should hide the menu.
273-
// // TODO: Change to global mousedown listener.
274-
// transaction.getMeta("focus") ||
275-
// transaction.getMeta("blur") ||
276-
// transaction.getMeta("pointer") ||
277-
// // Moving the caret before the character which triggered the menu should hide it.
278-
// (prev.triggerCharacter !== undefined &&
279-
// newState.selection.from < prev.queryStartPos()) ||
280-
// // Moving the caret to a new block should hide the menu.
281-
// !newState.selection.$from.sameParent(
282-
// newState.doc.resolve(prev.queryStartPos()),
283-
// )
284-
// ) {
285-
// return undefined;
286-
// }
287-
288-
// const next = { ...prev };
289-
// // here we wi
290-
// // Updates the current query.
291-
// next.query = newState.doc.textBetween(
292-
// prev.queryStartPos(),
293-
// newState.selection.from,
294-
// );
295-
296-
// return next;
297193
},
298194
},
299195

@@ -352,7 +248,7 @@ export class AutoCompleteProseMirrorPlugin<
352248
return null;
353249
}
354250

355-
console.log(autoCompleteState);
251+
// console.log(autoCompleteState);
356252
// Creates an inline decoration around the trigger character.
357253
return DecorationSet.create(state.doc, [
358254
Decoration.widget(
@@ -432,11 +328,6 @@ export interface DebouncedFunction<T extends any[], R> {
432328
cancel(): void;
433329
}
434330

435-
// TODO: more to blocknote API?
436-
// TODO: test with Collaboration edits
437-
// TODO: compare kilocode / cline etc
438-
// TODO: think about advanced scenarios (e.g.: multiple suggestions, etc.)
439-
// TODO: double tap -> extra long
440331
/**
441332
* Create a new AIExtension instance, this can be passed to the BlockNote editor via the `extensions` option
442333
*/
@@ -456,3 +347,9 @@ export function getAIAutoCompleteExtension(
456347
) {
457348
return editor.extension(AutoCompleteProseMirrorPlugin);
458349
}
350+
351+
// TODO: move more to blocknote API?
352+
// TODO: test with Collaboration edits
353+
// TODO: compare kilocode / cline etc
354+
// TODO: think about advanced scenarios (e.g.: multiple suggestions, etc.)
355+
// TODO: double tap -> insert extra long suggestion

0 commit comments

Comments
 (0)