Skip to content

Commit a6be872

Browse files
committed
Merge branch 'plugin-system' of github.com:TypeCellOS/BlockNote into plugin-system
2 parents 55ab3ce + 3bfbb00 commit a6be872

10 files changed

Lines changed: 108 additions & 96 deletions

File tree

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ export default function App() {
3535
ai: aiEn, // add default translations for the AI extension
3636
},
3737
// Register the AI extension
38-
extensions: [AIExtension()],
39-
ai: {
40-
transport: new DefaultChatTransport({
41-
// URL to your backend API, see example source in `packages/xl-ai-server/src/routes/regular.ts`
42-
api: `${BASE_URL}/regular/streamText`,
38+
extensions: [
39+
AIExtension({
40+
transport: new DefaultChatTransport({
41+
// URL to your backend API, see example source in `packages/xl-ai-server/src/routes/regular.ts`
42+
api: `${BASE_URL}/regular/streamText`,
43+
}),
4344
}),
44-
},
45+
],
4546
// We set some initial content for demo purposes
4647
initialContent: [
4748
{

examples/09-ai/02-playground/src/App.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,14 @@ export default function App() {
4545
ai: aiEn, // add default translations for the AI extension
4646
},
4747
// Register the AI extension
48-
extensions: [AIExtension()],
49-
ai: {
50-
transport: new DefaultChatTransport({
51-
// URL to your backend API, see example source in `packages/xl-ai-server/src/routes/regular.ts`
52-
api: `${BASE_URL}/model-playground/streamText`,
48+
extensions: [
49+
AIExtension({
50+
transport: new DefaultChatTransport({
51+
// URL to your backend API, see example source in `packages/xl-ai-server/src/routes/regular.ts`
52+
api: `${BASE_URL}/model-playground/streamText`,
53+
}),
5354
}),
54-
},
55+
],
5556
// We set some initial content for demo purposes
5657
initialContent: [
5758
{

examples/09-ai/03-custom-ai-menu-items/src/App.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ export default function App() {
3838
ai: aiEn, // add default translations for the AI extension
3939
},
4040
// Register the AI extension
41-
extensions: [AIExtension()],
42-
ai: {
43-
transport: new DefaultChatTransport({
44-
api: `${BASE_URL}/regular/streamText`,
41+
extensions: [
42+
AIExtension({
43+
transport: new DefaultChatTransport({
44+
api: `${BASE_URL}/regular/streamText`,
45+
}),
4546
}),
46-
},
47+
],
4748
// We set some initial content for demo purposes
4849
initialContent: [
4950
{

examples/09-ai/04-with-collaboration/src/App.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,13 @@ export default function App() {
8080
ai: aiEn, // add default translations for the AI extension
8181
},
8282
// Register the AI extension
83-
extensions: [AIExtension()],
84-
ai: {
85-
transport: new DefaultChatTransport({
86-
api: `${BASE_URL}/regular/streamText`,
83+
extensions: [
84+
AIExtension({
85+
transport: new DefaultChatTransport({
86+
api: `${BASE_URL}/regular/streamText`,
87+
}),
8788
}),
88-
},
89+
],
8990
// We set some initial content for demo purposes
9091
initialContent: [
9192
{

examples/09-ai/06-client-side-transport/src/App.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,16 @@ export default function App() {
4848
ai: aiEn, // add default translations for the AI extension
4949
},
5050
// Register the AI extension
51-
extensions: [AIExtension()],
52-
ai: {
53-
// The ClientSideTransport is used so the client makes calls directly to `streamText`
54-
// (whereas normally in the Vercel AI SDK, the client makes calls to your server, which then calls these methods)
55-
// (see https://github.com/vercel/ai/issues/5140 for background info)
56-
transport: new ClientSideTransport({
57-
model,
51+
extensions: [
52+
AIExtension({
53+
// The ClientSideTransport is used so the client makes calls directly to `streamText`
54+
// (whereas normally in the Vercel AI SDK, the client makes calls to your server, which then calls these methods)
55+
// (see https://github.com/vercel/ai/issues/5140 for background info)
56+
transport: new ClientSideTransport({
57+
model,
58+
}),
5859
}),
59-
},
60+
],
6061
// We set some initial content for demo purposes
6162
initialContent: [
6263
{

examples/09-ai/07-server-promptbuilder/src/App.tsx

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -36,50 +36,51 @@ export default function App() {
3636
ai: aiEn, // add default translations for the AI extension
3737
},
3838
// Register the AI extension
39-
extensions: [AIExtension()],
40-
ai: {
41-
// similar to https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence#sending-only-the-last-message
42-
// we adjust the transport to not send all messages to the backend
43-
transport: new DefaultChatTransport({
44-
// (see packages/xl-ai-server/src/routes/vercelAiSdkPersistence.ts)
45-
api: `${BASE_URL}/server-promptbuilder/streamText`,
46-
prepareSendMessagesRequest({ id, body, messages, requestMetadata }) {
47-
// we don't send the messages, just the information we need to compose / append messages server-side:
48-
// - the conversation id
49-
// - the promptData
50-
// - the tool results of the last message
39+
extensions: [
40+
AIExtension({
41+
// similar to https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence#sending-only-the-last-message
42+
// we adjust the transport to not send all messages to the backend
43+
transport: new DefaultChatTransport({
44+
// (see packages/xl-ai-server/src/routes/vercelAiSdkPersistence.ts)
45+
api: `${BASE_URL}/server-promptbuilder/streamText`,
46+
prepareSendMessagesRequest({ id, body, messages, requestMetadata }) {
47+
// we don't send the messages, just the information we need to compose / append messages server-side:
48+
// - the conversation id
49+
// - the promptData
50+
// - the tool results of the last message
5151

52-
// we need to share data about tool calls with the backend,
53-
// as these can be client-side executed. The backend needs to know the tool outputs
54-
// in order to compose a new valid LLM request
55-
const lastToolParts =
56-
messages.length > 0
57-
? messages[messages.length - 1].parts.filter((part) =>
58-
isToolOrDynamicToolUIPart(part),
59-
)
60-
: [];
52+
// we need to share data about tool calls with the backend,
53+
// as these can be client-side executed. The backend needs to know the tool outputs
54+
// in order to compose a new valid LLM request
55+
const lastToolParts =
56+
messages.length > 0
57+
? messages[messages.length - 1].parts.filter((part) =>
58+
isToolOrDynamicToolUIPart(part),
59+
)
60+
: [];
6161

62-
return {
63-
body: {
64-
...body,
65-
// TODO: this conversation id is client-side generated, we
66-
// should have a server-side generated id to ensure uniqueness
67-
// see https://github.com/vercel/ai/issues/7340#issuecomment-3307559636
68-
id,
69-
// get the promptData from requestMetadata (set by `promptAIRequestSender`) and send to backend
70-
promptData: (requestMetadata as any).promptData,
71-
lastToolParts,
72-
// messages, -> we explicitly don't send the messages array as we compose messages server-side
73-
},
74-
};
75-
},
62+
return {
63+
body: {
64+
...body,
65+
// TODO: this conversation id is client-side generated, we
66+
// should have a server-side generated id to ensure uniqueness
67+
// see https://github.com/vercel/ai/issues/7340#issuecomment-3307559636
68+
id,
69+
// get the promptData from requestMetadata (set by `promptAIRequestSender`) and send to backend
70+
promptData: (requestMetadata as any).promptData,
71+
lastToolParts,
72+
// messages, -> we explicitly don't send the messages array as we compose messages server-side
73+
},
74+
};
75+
},
76+
}),
77+
// customize the aiRequestSender to not update the messages array on the client-side
78+
aiRequestSender: defaultAIRequestSender(
79+
async () => {}, // disable the client-side promptbuilder
80+
aiDocumentFormats.html.defaultPromptInputDataBuilder,
81+
),
7682
}),
77-
// customize the aiRequestSender to not update the messages array on the client-side
78-
aiRequestSender: defaultAIRequestSender(
79-
async () => {}, // disable the client-side promptbuilder
80-
aiDocumentFormats.html.defaultPromptInputDataBuilder,
81-
),
82-
},
83+
],
8384
// We set some initial content for demo purposes
8485
initialContent: [
8586
{

packages/core/src/editor/BlockNoteExtension.test.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ it("creates an extension factory", () => {
1414
} as const;
1515
});
1616

17-
const factory = extension()({ editor });
18-
expect(factory.key).toBe("test");
19-
expect(factory.prosemirrorPlugins).toEqual([]);
17+
const extInstance = extension()({ editor });
18+
expect(extInstance.key).toBe("test");
19+
expect(extInstance.prosemirrorPlugins).toEqual([]);
2020
});
2121

2222
it("creates an extension factory with options", () => {
@@ -28,9 +28,9 @@ it("creates an extension factory with options", () => {
2828
} as const;
2929
});
3030

31-
const factory = extension({ x: 1 })({ editor });
32-
expect(factory.key).toBe("test");
33-
expect(factory.prosemirrorPlugins).toEqual([]);
31+
const extInstance = extension({ x: 1 })({ editor });
32+
expect(extInstance.key).toBe("test");
33+
expect(extInstance.prosemirrorPlugins).toEqual([]);
3434
});
3535

3636
it("creates an extension factory with undefined options", () => {
@@ -44,9 +44,9 @@ it("creates an extension factory with undefined options", () => {
4444
},
4545
);
4646

47-
const factory = extension()({ editor });
48-
expect(factory.key).toBe("test");
49-
expect(factory.prosemirrorPlugins).toEqual([]);
47+
const extInstance = extension()({ editor });
48+
expect(extInstance.key).toBe("test");
49+
expect(extInstance.prosemirrorPlugins).toEqual([]);
5050
});
5151

5252
it("creates an extension factory from an object", () => {
@@ -55,9 +55,9 @@ it("creates an extension factory from an object", () => {
5555
prosemirrorPlugins: [],
5656
} as const);
5757

58-
const factory = extension({ editor });
59-
expect(factory.key).toBe("test");
60-
expect(factory.prosemirrorPlugins).toEqual([]);
58+
const extInstance = extension({ editor });
59+
expect(extInstance.key).toBe("test");
60+
expect(extInstance.prosemirrorPlugins).toEqual([]);
6161
});
6262

6363
it("allows arbitrary properties on a no-options extension", () => {
@@ -72,13 +72,13 @@ it("allows arbitrary properties on a no-options extension", () => {
7272
} as const;
7373
});
7474

75-
const factory = extension()({ editor });
76-
expect(factory.arbitraryProperty).toBe("arbitraryValue");
77-
expect(factory.arbitraryMethod()).toBe("arbitraryValue");
75+
const extInstance = extension()({ editor });
76+
expect(extInstance.arbitraryProperty).toBe("arbitraryValue");
77+
expect(extInstance.arbitraryMethod()).toBe("arbitraryValue");
7878
// @ts-expect-error - this method takes no arguments
79-
factory.arbitraryMethod(90);
79+
extInstance.arbitraryMethod(90);
8080
// @ts-expect-error - this property is not defined
81-
factory.nonExistentProperty = "newArbitraryValue";
81+
extInstance.nonExistentProperty = "newArbitraryValue";
8282
});
8383

8484
it("allows arbitrary properties on an extension with options", () => {
@@ -94,10 +94,10 @@ it("allows arbitrary properties on an extension with options", () => {
9494
} as const;
9595
});
9696

97-
const factory = extension({ x: 1 })({ editor });
98-
expect(factory.arbitraryProperty).toBe("arbitraryValue");
97+
const extInstance = extension({ x: 1 })({ editor });
98+
expect(extInstance.arbitraryProperty).toBe("arbitraryValue");
9999
// @ts-expect-error - this method takes no arguments
100-
factory.arbitraryMethod(90);
100+
extInstance.arbitraryMethod(90);
101101
// @ts-expect-error - this property is not defined
102-
factory.nonExistentProperty = "newArbitraryValue";
102+
extInstance.nonExistentProperty = "newArbitraryValue";
103103
});

packages/core/src/editor/BlockNoteExtension.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,13 @@ export type ExtensionFactory<
174174
/**
175175
* Constructs a BlockNote {@link ExtensionFactory} from a factory function or object
176176
*/
177+
// This overload is for `createExtension({ key: "test", ... })`
177178
export function createExtension<
178179
const State = any,
179180
const Key extends string = string,
180181
const Ext extends Extension<State, Key> = Extension<State, Key>,
181182
>(factory: Ext): ExtensionFactoryInstance<Ext>;
183+
// This overload is for `createExtension(({editor, options}) => ({ key: "test", ... }))`
182184
export function createExtension<
183185
const State = any,
184186
const Options extends Record<string, any> | undefined = any,
@@ -187,6 +189,7 @@ export function createExtension<
187189
ctx: ExtensionOptions<Options>,
188190
) => Extension<State, Key>,
189191
>(factory: Factory): ExtensionFactory<State, Key, Factory>;
192+
// This overload is for both of the above overloads as it is the implementation of the function
190193
export function createExtension<
191194
const State = any,
192195
const Options extends Record<string, any> | undefined = any,
@@ -203,13 +206,17 @@ export function createExtension<
203206
: Factory extends (ctx: any) => Extension<State, Key>
204207
? ExtensionFactory<State, Key, Factory>
205208
: never {
206-
// TODO typing for this
207209
if (typeof factory === "object" && "key" in factory) {
208210
return function factoryFn() {
209211
(factory as any)[originalFactorySymbol] = factoryFn;
210212
return factory;
211213
} as any;
212214
}
215+
216+
if (typeof factory !== "function") {
217+
throw new Error("factory must be a function");
218+
}
219+
213220
return function factoryFn(options: Options) {
214221
return (ctx: { editor: BlockNoteEditor<any, any, any> }) => {
215222
const extension = factory({ editor: ctx.editor, options });

packages/core/src/editor/managers/ExtensionManager/extensions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export function getDefaultExtensions(
189189
SideMenu(options),
190190
SuggestionMenu(options),
191191
TrailingNode(),
192-
].filter((a) => a !== undefined) as ExtensionFactoryInstance[];
192+
] as ExtensionFactoryInstance[];
193193

194194
if (options.collaboration) {
195195
extensions.push(ForkYDoc(options.collaboration));
@@ -213,7 +213,7 @@ export function getDefaultExtensions(
213213
extensions.push(TableHandles(options));
214214
}
215215

216-
if (options.animations === false) {
216+
if (options.animations !== false) {
217217
extensions.push(PreviousBlockType());
218218
}
219219

packages/react/src/components/FormattingToolbar/ExperimentalMobileFormattingToolbarController.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export const ExperimentalMobileFormattingToolbarController = (props: {
2626

2727
const show = useExtensionState(FormattingToolbarExtension, {
2828
editor,
29-
selector: (state) => state,
3029
});
3130

3231
const style = useMemo<CSSProperties>(() => {

0 commit comments

Comments
 (0)