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
47 changes: 46 additions & 1 deletion examples/09-ai/01-minimal/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useCreateBlockNote,
} from "@blocknote/react";
import {
AIAutoCompleteExtension,
AIExtension,
AIMenuController,
AIToolbarButton,
Expand All @@ -22,12 +23,55 @@ import { en as aiEn } from "@blocknote/xl-ai/locales";
import "@blocknote/xl-ai/style.css";

import { DefaultChatTransport } from "ai";
import { useEffect } from "react";
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,
"\n",
);

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 @@ -43,6 +87,7 @@ export default function App() {
api: `${BASE_URL}/regular/streamText`,
}),
}),
AIAutoCompleteExtension({ autoCompleteProvider }),
],
// We set some initial content for demo purposes
initialContent: [
Expand Down
17 changes: 10 additions & 7 deletions packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export const SuggestionMenu = createExtension(({ editor }) => {
},

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 @@ -333,18 +333,21 @@ export const SuggestionMenu = createExtension(({ editor }) => {
: 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-persistence", serverPersistenceRoute);
app.route("/ai/model-playground", modelPlaygroundRoute);
app.route("/ai/autocomplete", autocompleteRoute);

const http2 = existsSync("localhost.pem");
serve(
Expand Down
55 changes: 55 additions & 0 deletions packages/xl-ai-server/src/routes/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createMistral } from "@ai-sdk/mistral";
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");

/**
* For this demo, we use `codestral-latest` from Mistral.
* It's originally designed for code completion, but it's
* performance make it a good candidate for fast text completions as well
*/
const model = createMistral({
apiKey: process.env.MISTRAL_API_KEY,
})("codestral-latest");

// 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, helping the user write text (NOT CODE). Predict and generate the most likely next part of the text.
- separate suggestions by newlines
- max 3 suggestions
- YOU MUST keep it short, USE MAXIMUM 5 (FIVE) 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),
});
});
2 changes: 2 additions & 0 deletions packages/xl-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export * from "./components/AIMenu/PromptSuggestionMenu.js";
export * from "./components/FormattingToolbar/AIToolbarButton.js";
export * from "./components/SuggestionMenu/getAISlashMenuItems.js";
export * from "./hooks/useAIDictionary.js";
export * from "./plugins/AutoCompletePlugin.js";
export * from "./server.js";
export * from "./streamTool/index.js";
Loading
Loading