Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AI block UI #980

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3e1983b
Added AI block
matthewlipski Aug 1, 2024
0da498e
Added inline and slash menu AI
matthewlipski Aug 6, 2024
d48d91e
Small fix
matthewlipski Aug 6, 2024
82a56a9
UX improvements & refactor
matthewlipski Aug 6, 2024
5c66cfe
Extracted AI to separate package & changed AI block toolbar UX
matthewlipski Sep 9, 2024
22db2b4
Finished initial package split
matthewlipski Sep 9, 2024
e0f60a8
Moved last AI references to AI package
matthewlipski Sep 9, 2024
a2bab5d
Reverted minor unneeded changes
matthewlipski Sep 9, 2024
bcf9d31
refactor architecture
YousefED Sep 10, 2024
b814336
add extensions
YousefED Sep 10, 2024
cfc1bed
Refactored AI dictionary
matthewlipski Sep 10, 2024
ec36733
clean dictionary
YousefED Sep 10, 2024
78ac784
fix
YousefED Sep 10, 2024
2970e9d
fix
YousefED Sep 10, 2024
78924bb
Made AI button use suggestion menu components
matthewlipski Sep 11, 2024
4083cd9
Added keyboard navigation to AI button
matthewlipski Sep 11, 2024
d0d82a4
Refactored AI button
matthewlipski Sep 11, 2024
2df84f5
Changed AI from suggestion menu to propriety menu
matthewlipski Sep 11, 2024
644aa15
Minor changes
matthewlipski Sep 11, 2024
0fcb46a
Prevented focus swapping on suggestion menu items
matthewlipski Sep 11, 2024
251e82b
- AI Menu input spans full block width
matthewlipski Sep 11, 2024
736a8ff
Fixed AI Menu position for empty blocks
matthewlipski Sep 11, 2024
ffa466d
Made AI block react instead of vanilla
matthewlipski Sep 12, 2024
2c20238
fix build
YousefED Sep 16, 2024
f474949
schema
YousefED Sep 17, 2024
6ddf7b0
improve json schema methods
YousefED Sep 18, 2024
34abf80
Merge remote-tracking branch 'origin/main' into ai-block
YousefED Sep 23, 2024
b3926fe
merge
YousefED Sep 23, 2024
a9d25c9
improve json schema methods
YousefED Sep 23, 2024
2021ce7
fix build
YousefED Sep 23, 2024
10c6f5e
WIP: schemas and selections
YousefED Sep 25, 2024
a554ff1
selections wip
YousefED Sep 25, 2024
6833d9d
update selections
YousefED Sep 25, 2024
39642ce
wip selectionmarkers
YousefED Sep 25, 2024
7693b10
drop core / react structure
YousefED Sep 26, 2024
a8752e1
ai menu
YousefED Sep 26, 2024
979b917
add comment
YousefED Sep 26, 2024
f88a986
misc
YousefED Sep 26, 2024
3b7a80b
Added `size` field to React suggestion menu items
matthewlipski Sep 26, 2024
5f20d34
Merge branch 'ai-block' of github.com:TypeCellOS/BlockNote into ai-block
YousefED Sep 27, 2024
bb02ee6
basis for accept / reject menu
YousefED Sep 27, 2024
da2f06d
selection commands
YousefED Sep 27, 2024
f7bbf0e
Added `.env` file for API key and AI menu buttons for after an AI com…
matthewlipski Sep 27, 2024
6952a2e
Merge remote-tracking branch 'origin/ai-block' into ai-block
matthewlipski Sep 27, 2024
3ae2152
Added loader to AI menu
matthewlipski Sep 27, 2024
08d31a9
Updated styles
matthewlipski Sep 29, 2024
b0993e2
wip
YousefED Sep 30, 2024
5367284
gitignore
YousefED Sep 30, 2024
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
4 changes: 4 additions & 0 deletions packages/ariakit/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ import { ToolbarSelect } from "./toolbar/ToolbarSelect";
import "./style.css";

export const components: Components = {
AIToolbar: {
Root: Toolbar,
Button: ToolbarButton,
},
FormattingToolbar: {
Root: Toolbar,
Button: ToolbarButton,
Expand Down
80 changes: 42 additions & 38 deletions packages/ariakit/src/toolbar/ToolbarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,45 +34,49 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
// assertEmpty in this case is only used at typescript level, not runtime level
assertEmpty(rest, false);

return (
<AriakitTooltipProvider>
<AriakitTooltipAnchor
className="link"
render={
<AriakitToolbarItem
aria-label={label}
className={mergeCSSClasses(
"bn-ak-button bn-ak-secondary",
className || ""
)}
// Needed as Safari doesn't focus button elements on mouse down
// unlike other browsers.
onMouseDown={(e) => {
if (isSafari()) {
(e.currentTarget as HTMLButtonElement).focus();
}
}}
onClick={onClick}
aria-pressed={isSelected}
data-selected={isSelected ? "true" : undefined}
data-test={
props.mainTooltip.slice(0, 1).toLowerCase() +
props.mainTooltip.replace(/\s+/g, "").slice(1)
}
// size={"xs"}
disabled={isDisabled || false}
ref={ref}
{...rest}>
{icon}
{children}
</AriakitToolbarItem>
const Button = (
<AriakitToolbarItem
aria-label={label}
className={mergeCSSClasses(
"bn-ak-button bn-ak-secondary",
className || ""
)}
// Needed as Safari doesn't focus button elements on mouse down
// unlike other browsers.
onMouseDown={(e) => {
if (isSafari()) {
(e.currentTarget as HTMLButtonElement).focus();
}
/>
<AriakitTooltip className="bn-ak-tooltip">
<span>{mainTooltip}</span>
{secondaryTooltip && <span>{secondaryTooltip}</span>}
</AriakitTooltip>
</AriakitTooltipProvider>
}}
onClick={onClick}
aria-pressed={isSelected}
data-selected={isSelected ? "true" : undefined}
data-test={
mainTooltip &&
mainTooltip.slice(0, 1).toLowerCase() +
mainTooltip.replace(/\s+/g, "").slice(1)
}
// size={"xs"}
disabled={isDisabled || false}
ref={ref}
{...rest}>
{icon}
{children}
</AriakitToolbarItem>
);

if (mainTooltip) {
return (
<AriakitTooltipProvider>
<AriakitTooltipAnchor className="link" render={Button} />
<AriakitTooltip className="bn-ak-tooltip">
<span>{mainTooltip}</span>
{secondaryTooltip && <span>{secondaryTooltip}</span>}
</AriakitTooltip>
</AriakitTooltipProvider>
);
}

return Button;
}
);
122 changes: 122 additions & 0 deletions packages/core/src/blocks/AIBlockContent/AIBlockContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
import {
BlockConfig,
BlockFromConfig,
createBlockSpec,
PropSchema,
} from "../../schema";
import { defaultProps } from "../defaultProps";

export const mockAIModelCall = async (_prompt: string) => {
return new Promise<string>((resolve) => {
setTimeout(() => {
resolve(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
);
}, 1000);
});
};

export const aiPropSchema = {
...defaultProps,
prompt: {
default: "" as const,
},
} satisfies PropSchema;

export const aiBlockConfig = {
type: "ai" as const,
propSchema: aiPropSchema,
content: "inline",
} satisfies BlockConfig;

export const aiRender = (
block: BlockFromConfig<typeof aiBlockConfig, any, any>,
editor: BlockNoteEditor<any, any, any>
) => {
if (!block.props.prompt) {
const generateResponseCallback = async () => {
generateButton.textContent = "Generating...";

editor.updateBlock(block, {
type: "ai",
props: { prompt: span.innerText },
content: await mockAIModelCall(block.props.prompt),
});
};

const promptBox = document.createElement("div");
promptBox.className = "bn-ai-prompt-box";

const icon = document.createElement("span");
icon.contentEditable = "false";
promptBox.appendChild(icon);
icon.outerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17.0007 1.20825 18.3195 3.68108 20.7923 4.99992 18.3195 6.31876 17.0007 8.79159 15.6818 6.31876 13.209 4.99992 15.6818 3.68108 17.0007 1.20825ZM8.00065 4.33325 10.6673 9.33325 15.6673 11.9999 10.6673 14.6666 8.00065 19.6666 5.33398 14.6666.333984 11.9999 5.33398 9.33325 8.00065 4.33325ZM19.6673 16.3333 18.0007 13.2083 16.334 16.3333 13.209 17.9999 16.334 19.6666 18.0007 22.7916 19.6673 19.6666 22.7923 17.9999 19.6673 16.3333Z"></path></svg>';

const span = document.createElement("span");
editor.domElement.addEventListener(
"keydown",
(event) => {
const currentBlock = editor.getTextCursorPosition().block;

if (
event.key === "Enter" &&
!editor.getSelection() &&
currentBlock.id === block.id &&
currentBlock.props.prompt === ""
) {
event.preventDefault();
event.stopPropagation();

generateResponseCallback();
}
},
true
);
promptBox.appendChild(span);

const generateButton = document.createElement("button");
generateButton.contentEditable = "false";
generateButton.textContent = "Generate";
generateButton.addEventListener("click", generateResponseCallback);
promptBox.appendChild(generateButton);

return {
dom: promptBox,
contentDOM: span,
};
}

const paragraph = document.createElement("p");

return {
dom: paragraph,
contentDOM: paragraph,
};
};

export const aiToExternalHTML = (
block: BlockFromConfig<typeof aiBlockConfig, any, any>
) => {
if (!block.props.prompt) {
const div = document.createElement("p");

return {
dom: div,
contentDOM: div,
};
}

const paragraph = document.createElement("p");

return {
dom: paragraph,
contentDOM: paragraph,
};
};

export const AIBlock = createBlockSpec(aiBlockConfig, {
render: aiRender,
toExternalHTML: aiToExternalHTML,
});
2 changes: 2 additions & 0 deletions packages/core/src/blocks/defaultBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { FileBlock } from "./FileBlockContent/FileBlockContent";
import { ImageBlock } from "./ImageBlockContent/ImageBlockContent";
import { VideoBlock } from "./VideoBlockContent/VideoBlockContent";
import { AudioBlock } from "./AudioBlockContent/AudioBlockContent";
import { AIBlock } from "./AIBlockContent/AIBlockContent";

export const defaultBlockSpecs = {
paragraph: Paragraph,
Expand All @@ -42,6 +43,7 @@ export const defaultBlockSpecs = {
image: ImageBlock,
video: VideoBlock,
audio: AudioBlock,
ai: AIBlock,
} satisfies BlockSpecs;

export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs);
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/editor/Block.css
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,49 @@ NESTED BLOCKS
font-style: italic;
}

/* AI */
[data-content-type="ai"] .bn-ai-prompt-box {
align-items: center;
border-radius: 8px;
display: flex;
flex-direction: row;
gap: 10px;
outline: solid 3px rgba(154, 56, 173, 0.2);
padding: 12px;
width: 100%;
}

[data-content-type="ai"] .bn-ai-prompt-box svg {
color: rgba(154, 56, 173, 0.2);
width: 24px;
height: 24px;
}

[data-content-type="ai"] .bn-ai-prompt-box span {
flex: 1;
}

[data-content-type="ai"] .bn-ai-prompt-box button {
background-color: transparent;
border: solid 1px rgba(120, 120, 120, 0.3);
border-radius: 4px;
color: rgba(154, 56, 173, 0.5);
cursor: pointer;
user-select: none;
}

[data-content-type="ai"][data-prompt] p {
border-radius: 4px;
}

[data-content-type="ai"][data-prompt] p:hover {
outline: solid 3px rgba(154, 56, 173, 0.1);
}

[data-content-type="ai"][data-prompt][data-is-focused] p {
outline: solid 3px rgba(154, 56, 173, 0.2);
}

/* TODO: should this be here? */

/* TEXT COLORS */
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
DefaultStyleSchema,
PartialBlock,
} from "../blocks/defaultBlocks";
import { AIToolbarProsemirrorPlugin } from "../extensions/AIToolbar/AIToolbarPlugin";
import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin";
import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin";
import { LinkToolbarProsemirrorPlugin } from "../extensions/LinkToolbar/LinkToolbarPlugin";
Expand Down Expand Up @@ -243,6 +244,7 @@ export class BlockNoteEditor<
ISchema,
SSchema
>;
public readonly aiToolbar?: AIToolbarProsemirrorPlugin;

/**
* The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload).
Expand Down Expand Up @@ -328,6 +330,9 @@ export class BlockNoteEditor<
if (checkDefaultBlockTypeInSchema("table", this)) {
this.tableHandles = new TableHandlesProsemirrorPlugin(this as any);
}
if (checkDefaultBlockTypeInSchema("ai", this)) {
this.aiToolbar = new AIToolbarProsemirrorPlugin();
}

const extensions = getBlockNoteExtensions({
editor: this,
Expand All @@ -351,6 +356,7 @@ export class BlockNoteEditor<
this.suggestionMenus.plugin,
...(this.filePanel ? [this.filePanel.plugin] : []),
...(this.tableHandles ? [this.tableHandles.plugin] : []),
...(this.aiToolbar ? [this.aiToolbar.plugin] : []),
PlaceholderPlugin(this, newOptions.placeholders),
];
},
Expand Down
Loading
Loading