Skip to content

Commit d0c34a2

Browse files
committed
wip
1 parent dbef5e8 commit d0c34a2

21 files changed

Lines changed: 1214 additions & 6 deletions

package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"@tiptap/pm": "^2.11.5",
8686
"emoji-mart": "^5.6.0",
8787
"hast-util-from-dom": "^4.2.0",
88+
"@handlewithcare/prosemirror-suggest-changes": "^0.1.3",
8889
"prosemirror-dropcursor": "^1.8.1",
8990
"prosemirror-highlight": "^0.9.0",
9091
"prosemirror-model": "^1.24.1",

packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
2525
import { getNodeById } from "../../../nodeUtil.js";
2626

27-
function updateBlockTr<
27+
export function updateBlockTr<
2828
BSchema extends BlockSchema,
2929
I extends InlineContentSchema,
3030
S extends StyleSchema

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ export class BlockNoteEditor<
683683
// but we still need the schema
684684
this.pmSchema = getSchema(tiptapOptions.extensions!);
685685
}
686+
686687
this.emit("create");
687688
}
688689

packages/core/src/editor/BlockNoteExtensions.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import * as Y from "yjs";
1010
import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDropExtension.js";
1111
import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension.js";
1212
import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js";
13+
import type { ThreadStore } from "../comments/index.js";
14+
import { AgentCursorPlugin } from "../extensions/AgentCursor/AgentCursorPlugin.js";
1315
import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js";
1416
import { createCollaborationExtensions } from "../extensions/Collaboration/createCollaborationExtensions.js";
1517
import { CommentMark } from "../extensions/Comments/CommentMark.js";
1618
import { CommentsPlugin } from "../extensions/Comments/CommentsPlugin.js";
17-
import type { ThreadStore } from "../comments/index.js";
1819
import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin.js";
1920
import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin.js";
2021
import { KeyboardShortcutsExtension } from "../extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js";
@@ -29,6 +30,11 @@ import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/Previou
2930
import { ShowSelectionPlugin } from "../extensions/ShowSelection/ShowSelectionPlugin.js";
3031
import { SideMenuProsemirrorPlugin } from "../extensions/SideMenu/SideMenuPlugin.js";
3132
import { SuggestionMenuProseMirrorPlugin } from "../extensions/SuggestionMenu/SuggestionPlugin.js";
33+
import {
34+
SuggestionAddMark,
35+
SuggestionDeleteMark,
36+
SuggestionModificationMark,
37+
} from "../extensions/Suggestions/SuggestionMarks.js";
3238
import { TableHandlesProsemirrorPlugin } from "../extensions/TableHandles/TableHandlesPlugin.js";
3339
import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension.js";
3440
import { TextColorExtension } from "../extensions/TextColor/TextColorExtension.js";
@@ -143,6 +149,8 @@ export const getBlockNoteExtensions = <
143149
);
144150
}
145151

152+
ret["agentCursor"] = new AgentCursorPlugin(opts.editor);
153+
146154
const disableExtensions: string[] = opts.disableExtensions || [];
147155
for (const ext of disableExtensions) {
148156
delete ret[ext];
@@ -186,6 +194,9 @@ const getTipTapExtensions = <
186194
Text,
187195

188196
// marks:
197+
SuggestionAddMark,
198+
SuggestionDeleteMark,
199+
SuggestionModificationMark,
189200
Link.extend({
190201
inclusive: false,
191202
}).configure({
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Plugin, PluginKey } from "prosemirror-state";
2+
import { Decoration, DecorationSet } from "prosemirror-view";
3+
import { defaultSelectionBuilder } from "y-prosemirror";
4+
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
5+
6+
type AgentCursorState = {
7+
selection: { anchor: number; head: number } | undefined;
8+
};
9+
const PLUGIN_KEY = new PluginKey<AgentCursorState>(`blocknote-agent-cursor`);
10+
11+
const user = {
12+
name: "AI",
13+
color: "#579ed7",
14+
};
15+
16+
// TODO: move to xl-ai?
17+
export class AgentCursorPlugin {
18+
public readonly plugin: Plugin;
19+
constructor(editor: BlockNoteEditor<any, any, any>) {
20+
this.plugin = new Plugin<AgentCursorState>({
21+
key: PLUGIN_KEY,
22+
view: (view) => {
23+
return {};
24+
},
25+
state: {
26+
init: () => {
27+
return {
28+
selection: undefined,
29+
};
30+
},
31+
apply: (tr, oldState) => {
32+
const meta = tr.getMeta("aiAgent");
33+
34+
if (!meta) {
35+
return {
36+
selection: undefined,
37+
};
38+
}
39+
40+
return {
41+
selection: meta.selection,
42+
};
43+
},
44+
},
45+
props: {
46+
decorations: (state) => {
47+
const { doc } = state;
48+
49+
const { selection } = PLUGIN_KEY.getState(state)!;
50+
51+
const decs = [];
52+
53+
if (!selection) {
54+
return DecorationSet.create(doc, []);
55+
}
56+
57+
decs.push(
58+
Decoration.widget(selection.head, () => renderCursor(user), {
59+
key: "agent-cursor",
60+
side: 10,
61+
})
62+
);
63+
64+
const from = Math.min(selection.anchor, selection.head);
65+
const to = Math.max(selection.anchor, selection.head);
66+
67+
decs.push(
68+
Decoration.inline(from, to, defaultSelectionBuilder(user), {
69+
inclusiveEnd: true,
70+
inclusiveStart: false,
71+
})
72+
);
73+
74+
return DecorationSet.create(doc, decs);
75+
},
76+
},
77+
});
78+
}
79+
}
80+
81+
const renderCursor = (user: { name: string; color: string }) => {
82+
const cursorElement = document.createElement("span");
83+
84+
cursorElement.classList.add("bn-collaboration-cursor__base");
85+
cursorElement.setAttribute("data-active", "true");
86+
87+
const caretElement = document.createElement("span");
88+
caretElement.setAttribute("contentedEditable", "false");
89+
caretElement.classList.add("bn-collaboration-cursor__caret");
90+
caretElement.setAttribute("style", `background-color: ${user.color}`);
91+
92+
const labelElement = document.createElement("span");
93+
94+
labelElement.classList.add("bn-collaboration-cursor__label");
95+
labelElement.setAttribute("style", `background-color: ${user.color}`);
96+
labelElement.insertBefore(document.createTextNode(user.name), null);
97+
98+
caretElement.insertBefore(labelElement, null);
99+
100+
cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
101+
cursorElement.insertBefore(caretElement, null);
102+
cursorElement.insertBefore(document.createTextNode("\u2060"), null); // Non-breaking space
103+
104+
return cursorElement;
105+
};
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { Mark } from "@tiptap/core";
2+
3+
export const SuggestionAddMark = Mark.create({
4+
name: "insertion",
5+
inclusive: false,
6+
extendMarkSchema(extension) {
7+
if (extension.name !== "insertion") {
8+
return {};
9+
}
10+
return {
11+
blocknoteIgnore: true,
12+
inclusive: false,
13+
excludes: "deletion modification insertion",
14+
attrs: {
15+
id: { validate: "number" },
16+
},
17+
toDOM(mark, inline) {
18+
return [
19+
"ins",
20+
{
21+
"data-id": String(mark.attrs["id"]),
22+
"data-inline": String(inline),
23+
...(!inline && { style: "display: block" }),
24+
},
25+
0,
26+
];
27+
},
28+
parseDOM: [
29+
{
30+
tag: "ins",
31+
getAttrs(node) {
32+
if (!node.dataset["id"]) return false;
33+
return {
34+
id: parseInt(node.dataset["id"], 10),
35+
};
36+
},
37+
},
38+
],
39+
};
40+
},
41+
});
42+
43+
export const SuggestionDeleteMark = Mark.create({
44+
name: "deletion",
45+
inclusive: false,
46+
47+
extendMarkSchema(extension) {
48+
if (extension.name !== "deletion") {
49+
return {};
50+
}
51+
return {
52+
blocknoteIgnore: true,
53+
inclusive: false,
54+
55+
excludes: "insertion modification deletion",
56+
attrs: {
57+
id: { validate: "number" },
58+
},
59+
toDOM(mark, inline) {
60+
return [
61+
"del",
62+
{
63+
"data-id": String(mark.attrs["id"]),
64+
"data-inline": String(inline),
65+
...(!inline && { style: "display: block" }),
66+
},
67+
0,
68+
];
69+
},
70+
parseDOM: [
71+
{
72+
tag: "del",
73+
getAttrs(node) {
74+
if (!node.dataset["id"]) return false;
75+
return {
76+
id: parseInt(node.dataset["id"], 10),
77+
};
78+
},
79+
},
80+
],
81+
};
82+
},
83+
});
84+
85+
export const SuggestionModificationMark = Mark.create({
86+
name: "modification",
87+
inclusive: false,
88+
89+
extendMarkSchema(extension) {
90+
if (extension.name !== "modification") {
91+
return {};
92+
}
93+
return {
94+
blocknoteIgnore: true,
95+
inclusive: false,
96+
excludes: "deletion insertion",
97+
attrs: {
98+
id: { validate: "number" },
99+
type: { validate: "string" },
100+
attrName: { default: null, validate: "string|null" },
101+
previousValue: { default: null },
102+
newValue: { default: null },
103+
},
104+
toDOM(mark, inline) {
105+
return [
106+
inline ? "span" : "div",
107+
{
108+
"data-type": "modification",
109+
"data-id": String(mark.attrs["id"]),
110+
"data-mod-type": mark.attrs["type"] as string,
111+
"data-mod-prev-val": JSON.stringify(mark.attrs["previousValue"]),
112+
// TODO: Try to serialize marks with toJSON?
113+
"data-mod-new-val": JSON.stringify(mark.attrs["newValue"]),
114+
},
115+
0,
116+
];
117+
},
118+
parseDOM: [
119+
{
120+
tag: "span[data-type='modification']",
121+
getAttrs(node) {
122+
if (!node.dataset["id"]) return false;
123+
return {
124+
id: parseInt(node.dataset["id"], 10),
125+
type: node.dataset["modType"],
126+
previousValue: node.dataset["modPrevVal"],
127+
newValue: node.dataset["modNewVal"],
128+
};
129+
},
130+
},
131+
{
132+
tag: "div[data-type='modification']",
133+
getAttrs(node) {
134+
if (!node.dataset["id"]) return false;
135+
return {
136+
id: parseInt(node.dataset["id"], 10),
137+
type: node.dataset["modType"],
138+
previousValue: node.dataset["modPrevVal"],
139+
};
140+
},
141+
},
142+
],
143+
};
144+
},
145+
});

packages/xl-ai/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
"_prepare": "npm run build"
5252
},
5353
"dependencies": {
54+
"@emergence-engineering/prosemirror-text-map": "^0.1.4",
55+
"prosemirror-changeset": "^2.2.1",
5456
"@ewoudenberg/difflib": "^0.1.0",
5557
"@ai-sdk/openai": "^1.1.0",
5658
"@ai-sdk/groq": "^1.1.0",

0 commit comments

Comments
 (0)