Skip to content

Commit 2df84f5

Browse files
committed
Changed AI from suggestion menu to propriety menu
1 parent d0d82a4 commit 2df84f5

File tree

15 files changed

+455
-173
lines changed

15 files changed

+455
-173
lines changed

examples/01-basic/01-minimal/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
AIBlockToolbarProsemirrorPlugin,
44
AIButton,
55
AIInlineToolbarProsemirrorPlugin,
6+
AIMenuProsemirrorPlugin,
67
BlockNoteAIUI,
78
aiBlockTypeSelectItems,
89
en as aiEN,
@@ -31,8 +32,8 @@ import {
3132

3233
const schema = BlockNoteSchema.create({
3334
blockSpecs: {
34-
ai: AIBlock,
3535
...defaultBlockSpecs,
36+
ai: AIBlock,
3637
},
3738
});
3839
export default function App() {
@@ -47,6 +48,7 @@ export default function App() {
4748
// TODO: things will break when user provides different keys. Define name on plugins instead?
4849
aiBlockToolbar: new AIBlockToolbarProsemirrorPlugin(),
4950
aiInlineToolbar: new AIInlineToolbarProsemirrorPlugin(),
51+
aiMenu: new AIMenuProsemirrorPlugin(),
5052
},
5153
});
5254

packages/ai/src/core/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class AIBlockToolbarView implements PluginView {
5151
(editorWrapper === (event.relatedTarget as Node) ||
5252
editorWrapper.contains(event.relatedTarget as Node) ||
5353
(event.relatedTarget as HTMLElement).matches(
54-
".bn-ui-container, .bn-ui-container *"
54+
".bn-container, .bn-container *"
5555
))
5656
) {
5757
return;

packages/ai/src/core/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class AIInlineToolbarView implements PluginView {
4949
(editorWrapper === (event.relatedTarget as Node) ||
5050
editorWrapper.contains(event.relatedTarget as Node) ||
5151
(event.relatedTarget as HTMLElement).matches(
52-
".bn-ui-container, .bn-ui-container *"
52+
".bn-container, .bn-container *"
5353
))
5454
) {
5555
return;
@@ -116,7 +116,6 @@ export class AIInlineToolbarView implements PluginView {
116116
}
117117

118118
open(prompt: string, operation: "replaceSelection" | "insertAfterSelection") {
119-
this.pmView.focus();
120119
this.pmView.dispatch(
121120
this.pmView.state.tr.scrollIntoView().setMeta(aiInlineToolbarPluginKey, {
122121
open: true,
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import {
2+
EventEmitter,
3+
getBlockInfoFromPos,
4+
UiElementPosition,
5+
} from "@blocknote/core";
6+
import { Plugin, PluginKey, PluginView } from "prosemirror-state";
7+
import { EditorView } from "prosemirror-view";
8+
9+
export type AIMenuState = UiElementPosition;
10+
11+
export class AIMenuView implements PluginView {
12+
public state?: AIMenuState;
13+
public emitUpdate: () => void;
14+
15+
public domElement: HTMLElement | undefined;
16+
17+
constructor(
18+
private readonly pmView: EditorView,
19+
emitUpdate: (state: AIMenuState) => void
20+
) {
21+
this.emitUpdate = () => {
22+
if (!this.state) {
23+
throw new Error("Attempting to update uninitialized AI menu");
24+
}
25+
26+
emitUpdate(this.state);
27+
};
28+
29+
pmView.dom.addEventListener("dragstart", this.dragHandler);
30+
pmView.dom.addEventListener("dragover", this.dragHandler);
31+
pmView.dom.addEventListener("blur", this.blurHandler);
32+
pmView.dom.addEventListener("mousedown", this.closeHandler, true);
33+
34+
// Setting capture=true ensures that any parent container of the editor that
35+
// gets scrolled will trigger the scroll event. Scroll events do not bubble
36+
// and so won't propagate to the document by default.
37+
pmView.root.addEventListener("scroll", this.scrollHandler, true);
38+
}
39+
40+
blurHandler = (event: FocusEvent) => {
41+
const editorWrapper = this.pmView.dom.parentElement!;
42+
43+
// Checks if the focus is moving to an element outside the editor. If it is,
44+
// the menu is hidden.
45+
if (
46+
// An element is clicked.
47+
event &&
48+
event.relatedTarget &&
49+
// Element is inside the editor.
50+
(editorWrapper === (event.relatedTarget as Node) ||
51+
editorWrapper.contains(event.relatedTarget as Node) ||
52+
(event.relatedTarget as HTMLElement).matches(
53+
".bn-container, .bn-container *"
54+
))
55+
) {
56+
return;
57+
}
58+
59+
if (this.state?.show) {
60+
this.state.show = false;
61+
this.emitUpdate();
62+
}
63+
};
64+
65+
// For dragging the whole editor.
66+
dragHandler = () => {
67+
if (this.state?.show) {
68+
this.state.show = false;
69+
this.emitUpdate();
70+
}
71+
};
72+
73+
closeHandler = () => this.close();
74+
75+
scrollHandler = () => {
76+
if (this.state?.show) {
77+
this.state.referencePos = this.domElement!.getBoundingClientRect();
78+
this.emitUpdate();
79+
}
80+
};
81+
82+
update(view: EditorView) {
83+
const pluginState: AIInlineToolbarPluginState = aiMenuPluginKey.getState(
84+
view.state
85+
);
86+
87+
if (this.state && !this.state.show && !pluginState.open) {
88+
return;
89+
}
90+
91+
if (pluginState.open) {
92+
const blockInfo = getBlockInfoFromPos(
93+
view.state.doc,
94+
view.state.selection.from
95+
);
96+
97+
this.domElement = view.domAtPos(blockInfo.startPos).node
98+
.firstChild as HTMLElement;
99+
100+
this.state = {
101+
show: true,
102+
referencePos: this.domElement.getBoundingClientRect(),
103+
};
104+
105+
this.emitUpdate();
106+
107+
return;
108+
}
109+
110+
if (this.state?.show) {
111+
this.state.show = false;
112+
this.emitUpdate();
113+
}
114+
}
115+
116+
destroy() {
117+
this.pmView.dom.removeEventListener("dragstart", this.dragHandler);
118+
this.pmView.dom.removeEventListener("dragover", this.dragHandler);
119+
this.pmView.dom.removeEventListener("blur", this.blurHandler);
120+
this.pmView.dom.removeEventListener("mousedown", this.closeHandler);
121+
122+
this.pmView.root.removeEventListener("scroll", this.scrollHandler, true);
123+
}
124+
125+
open() {
126+
this.pmView.focus();
127+
this.pmView.dispatch(
128+
this.pmView.state.tr.scrollIntoView().setMeta(aiMenuPluginKey, {
129+
open: true,
130+
})
131+
);
132+
}
133+
134+
close() {
135+
this.pmView.focus();
136+
this.pmView.dispatch(
137+
this.pmView.state.tr.scrollIntoView().setMeta(aiMenuPluginKey, {
138+
open: false,
139+
})
140+
);
141+
}
142+
143+
closeMenu = () => {
144+
if (this.state?.show) {
145+
this.state.show = false;
146+
this.emitUpdate();
147+
}
148+
};
149+
}
150+
151+
type AIInlineToolbarPluginState = {
152+
open: boolean;
153+
};
154+
155+
export const aiMenuPluginKey = new PluginKey("AIMenuPlugin");
156+
157+
export class AIMenuProsemirrorPlugin extends EventEmitter<any> {
158+
private view: AIMenuView | undefined;
159+
public readonly plugin: Plugin;
160+
constructor() {
161+
super();
162+
163+
this.plugin = new Plugin({
164+
key: aiMenuPluginKey,
165+
166+
view: (editorView) => {
167+
this.view = new AIMenuView(editorView, (state) => {
168+
this.emit("update", state);
169+
});
170+
return this.view;
171+
},
172+
173+
state: {
174+
// Initialize the plugin's internal state.
175+
init(): AIInlineToolbarPluginState {
176+
return { open: false };
177+
},
178+
179+
// Apply changes to the plugin state from an editor transaction.
180+
apply(transaction, prev) {
181+
const meta: AIInlineToolbarPluginState | undefined =
182+
transaction.getMeta(aiMenuPluginKey);
183+
184+
if (meta === undefined) {
185+
return prev;
186+
}
187+
188+
return meta;
189+
},
190+
},
191+
});
192+
}
193+
194+
public open() {
195+
this.view?.open();
196+
}
197+
198+
public close() {
199+
this.view?.close();
200+
}
201+
202+
public get shown() {
203+
return this.view?.state?.show || false;
204+
}
205+
206+
public onUpdate(callback: (state: AIMenuState) => void) {
207+
return this.on("update", callback);
208+
}
209+
210+
public closeMenu = () => this.view!.closeMenu();
211+
}

packages/ai/src/core/i18n/locales/en.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,26 @@ export const en = {
2323
ai: "Enter a prompt",
2424
},
2525
ai_menu: {
26-
custom_prompt: {
27-
title: "Custom Prompt",
28-
subtext: "Use your query as an AI prompt",
29-
aliases: [
30-
"", // TODO: add comment
31-
"custom prompt",
32-
],
33-
},
26+
// custom_prompt: {
27+
// title: "Custom Prompt",
28+
// subtext: "Use your query as an AI prompt",
29+
// aliases: [
30+
// "", // TODO: add comment
31+
// "custom prompt",
32+
// ],
33+
// },
3434
make_longer: {
3535
title: "Make Longer",
3636
aliases: ["make longer"],
3737
},
38+
make_shorter: {
39+
title: "Make Shorter",
40+
aliases: ["make shorter"],
41+
},
42+
rewrite: {
43+
title: "Rewrite",
44+
aliases: ["rewrite"],
45+
},
3846
},
3947
ai_block_toolbar: {
4048
show_prompt: "Generated by AI",

packages/ai/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from "./blocks/AIBlockContent/mockAIFunctions";
33

44
export * from "./extensions/AIBlockToolbar/AIBlockToolbarPlugin";
55
export * from "./extensions/AIInlineToolbar/AIInlineToolbarPlugin";
6+
export * from "./extensions/AIMenu/AIMenuPlugin";
67
export * from "./extensions/SuggestionMenu/getDefaultSlashMenuItems";
78

89
export * from "./i18n/dictionary";

0 commit comments

Comments
 (0)