Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/09-ai/01-minimal/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import {
AIMenuController,
AIToolbarButton,
createAIAutoCompleteExtension,
createAIExtension,
getAISlashMenuItems,
} from "@blocknote/xl-ai";
Expand All @@ -26,6 +27,49 @@ import { getEnv } from "./getEnv";
const BASE_URL =
getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";

/**
* Fetches suggestions for the auto complete plugin from our backend API.
*/
async function autoCompleteProvider(
editor: BlockNoteEditor<any, any, any>,
signal: AbortSignal,
) {
// TODO:
// - API is very prosemirror-based, make something more BlockNote-native
// - Add simple method to retrieve relevant context (e.g. block content / json until selection)

const state = editor.prosemirrorState;
const text = state.doc.textBetween(
state.selection.from - 300,
state.selection.from,
);

const response = await fetch(
"https://blocknote-pr-2191.onrender.com/ai/autocomplete/generateText",
// `https://localhost:3000/ai/autocomplete/generateText`,
{
method: "POST",
body: JSON.stringify({ text }),
signal,
},
);
const data = await response.json();
return data.suggestions.map((suggestion: string) => ({
position: state.selection.from,
suggestion: suggestion,
}));
// return [
// {
// position: state.selection.from,
// suggestion: "Hello World",
// },
// {
// position: state.selection.from,
// suggestion: "Hello Planet",
// },
// ];
}

export default function App() {
// Creates a new editor instance.
const editor = useCreateBlockNote({
Expand All @@ -41,6 +85,7 @@ export default function App() {
api: `${BASE_URL}/regular/streamText`,
}),
}),
createAIAutoCompleteExtension({ autoCompleteProvider }),
],
// We set some initial content for demo purposes
initialContent: [
Expand Down
17 changes: 10 additions & 7 deletions packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class SuggestionMenuProseMirrorPlugin<
},

props: {
handleTextInput(view, from, to, text) {
handleTextInput(view, from, to, text, deflt) {
// only on insert
if (from === to) {
const doc = view.state.doc;
Expand All @@ -301,18 +301,21 @@ export class SuggestionMenuProseMirrorPlugin<
: text;

if (str === snippet) {
view.dispatch(view.state.tr.insertText(text));
view.dispatch(
view.state.tr
.setMeta(suggestionMenuPluginKey, {
triggerCharacter: snippet,
})
.scrollIntoView(),
deflt().setMeta(suggestionMenuPluginKey, {
triggerCharacter: snippet,
}),
);
return true;
}
}
}
if (this.getState(view.state)) {
// when menu is open, we dispatch the default transaction
// and return true so that other event handlers (i.e.: AI AutoComplete) are not triggered
view.dispatch(deflt());
return true;
}
return false;
},

Expand Down
2 changes: 2 additions & 0 deletions packages/xl-ai-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cors } from "hono/cors";
import { existsSync, readFileSync } from "node:fs";
import { createSecureServer } from "node:http2";
import { Agent, setGlobalDispatcher } from "undici";
import { autocompleteRoute } from "./routes/autocomplete.js";
import { modelPlaygroundRoute } from "./routes/model-playground/index.js";
import { objectGenerationRoute } from "./routes/objectGeneration.js";
import { proxyRoute } from "./routes/proxy.js";
Expand Down Expand Up @@ -37,6 +38,7 @@ app.route("/ai/proxy", proxyRoute);
app.route("/ai/object-generation", objectGenerationRoute);
app.route("/ai/server-promptbuilder", serverPromptbuilderRoute);
app.route("/ai/model-playground", modelPlaygroundRoute);
app.route("/ai/autocomplete", autocompleteRoute);

const http2 = existsSync("localhost.pem");
serve(
Expand Down
46 changes: 46 additions & 0 deletions packages/xl-ai-server/src/routes/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createGroq } from "@ai-sdk/groq";
import { generateText } from "ai";
import { Hono } from "hono";

export const autocompleteRoute = new Hono();

// Setup your model
// const model = createOpenAI({
// apiKey: process.env.OPENAI_API_KEY,
// })("gpt-4.1-nano");

const model = createGroq({
apiKey: process.env.GROQ_API_KEY,
})("openai/gpt-oss-20b");

// Use `streamText` to stream text responses from the LLM
autocompleteRoute.post("/generateText", async (c) => {
const { text } = await c.req.json();

const result = await generateText({
model,
system: `You are a writing assistant. Predict and generate the most likely next part of the text.
- separate suggestions by newlines
- max 3 suggestions
- keep it short, max 5 words per suggestion
- don't include other text (or explanations)
- YOU MUST ONLY return the text to be appended. Your suggestion will EXACTLY replace [SUGGESTION_HERE].
- YOU MUST NOT include the original text / characters (prefix) in your suggestion.
- 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)`,
messages: [
{
role: "user",
content: `Complete the following text:
${text}[SUGGESTION_HERE]`,
},
],
abortSignal: c.req.raw.signal,
});

return c.json({
suggestions: result.text
.split("\n")
.map((suggestion) => suggestion.trimEnd())
.filter((suggestion) => suggestion.trim().length > 0),
});
});
1 change: 1 addition & 0 deletions packages/xl-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from "./components/AIMenu/PromptSuggestionMenu.js";
export * from "./components/FormattingToolbar/AIToolbarButton.js";
export * from "./components/SuggestionMenu/getAISlashMenuItems.js";
export * from "./i18n/dictionary.js";
export * from "./plugins/AutoCompletePlugin.js";
export * from "./streamTool/index.js";
Loading
Loading