From 3e1983b9a46404cf7455e9f80c0a1981718991dc Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Thu, 1 Aug 2024 16:06:26 +0200 Subject: [PATCH 01/45] Added AI block --- packages/ariakit/src/index.tsx | 4 + .../ariakit/src/toolbar/ToolbarButton.tsx | 80 +++++---- .../blocks/AIBlockContent/AIBlockContent.ts | 122 +++++++++++++ packages/core/src/blocks/defaultBlocks.ts | 2 + packages/core/src/editor/Block.css | 43 +++++ packages/core/src/editor/BlockNoteEditor.ts | 6 + .../extensions/AIToolbar/AIToolbarPlugin.ts | 167 ++++++++++++++++++ .../Placeholder/PlaceholderPlugin.ts | 17 +- .../getDefaultSlashMenuItems.ts | 12 ++ packages/core/src/i18n/locales/ar.ts | 12 ++ packages/core/src/i18n/locales/en.ts | 12 ++ packages/core/src/i18n/locales/fr.ts | 12 ++ packages/core/src/i18n/locales/is.ts | 12 ++ packages/core/src/i18n/locales/ja.ts | 12 ++ packages/core/src/i18n/locales/ko.ts | 12 ++ packages/core/src/i18n/locales/nl.ts | 12 ++ packages/core/src/i18n/locales/pl.ts | 12 ++ packages/core/src/i18n/locales/pt.ts | 12 ++ packages/core/src/i18n/locales/ru.ts | 12 ++ packages/core/src/i18n/locales/vi.ts | 12 ++ packages/core/src/i18n/locales/zh.ts | 12 ++ packages/core/src/index.ts | 1 + packages/mantine/src/index.tsx | 4 + .../mantine/src/toolbar/ToolbarButton.tsx | 16 +- .../src/components/AIToolbar/AIToolbar.tsx | 37 ++++ .../AIToolbar/AIToolbarController.tsx | 59 +++++++ .../components/AIToolbar/AIToolbarProps.ts | 3 + .../DefaultButtons/ShowPromptButton.tsx | 102 +++++++++++ .../AIToolbar/DefaultButtons/UpdateButton.tsx | 49 +++++ .../DefaultSelects/BlockTypeSelect.tsx | 7 + .../src/components/SideMenu/SideMenu.tsx | 4 + .../getDefaultReactSlashMenuItems.tsx | 4 +- .../react/src/editor/BlockNoteDefaultUI.tsx | 3 + .../react/src/editor/ComponentsContext.tsx | 22 ++- packages/react/src/editor/styles.css | 4 + packages/shadcn/src/index.tsx | 4 + packages/shadcn/src/toolbar/Toolbar.tsx | 28 +-- 37 files changed, 877 insertions(+), 67 deletions(-) create mode 100644 packages/core/src/blocks/AIBlockContent/AIBlockContent.ts create mode 100644 packages/core/src/extensions/AIToolbar/AIToolbarPlugin.ts create mode 100644 packages/react/src/components/AIToolbar/AIToolbar.tsx create mode 100644 packages/react/src/components/AIToolbar/AIToolbarController.tsx create mode 100644 packages/react/src/components/AIToolbar/AIToolbarProps.ts create mode 100644 packages/react/src/components/AIToolbar/DefaultButtons/ShowPromptButton.tsx create mode 100644 packages/react/src/components/AIToolbar/DefaultButtons/UpdateButton.tsx diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx index e766e2c65..880bf2199 100644 --- a/packages/ariakit/src/index.tsx +++ b/packages/ariakit/src/index.tsx @@ -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, diff --git a/packages/ariakit/src/toolbar/ToolbarButton.tsx b/packages/ariakit/src/toolbar/ToolbarButton.tsx index f19b2f9ca..d0027be9c 100644 --- a/packages/ariakit/src/toolbar/ToolbarButton.tsx +++ b/packages/ariakit/src/toolbar/ToolbarButton.tsx @@ -34,45 +34,49 @@ export const ToolbarButton = forwardRef( // assertEmpty in this case is only used at typescript level, not runtime level assertEmpty(rest, false); - return ( - - { - 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} - + const Button = ( + { + if (isSafari()) { + (e.currentTarget as HTMLButtonElement).focus(); } - /> - - {mainTooltip} - {secondaryTooltip && {secondaryTooltip}} - - + }} + 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} + ); + + if (mainTooltip) { + return ( + + + + {mainTooltip} + {secondaryTooltip && {secondaryTooltip}} + + + ); + } + + return Button; } ); diff --git a/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts b/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts new file mode 100644 index 000000000..55c32a772 --- /dev/null +++ b/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts @@ -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((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, + editor: BlockNoteEditor +) => { + 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 = + ''; + + 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 +) => { + 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, +}); diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index e2b9b9e8d..748ea3957 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -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, @@ -42,6 +43,7 @@ export const defaultBlockSpecs = { image: ImageBlock, video: VideoBlock, audio: AudioBlock, + ai: AIBlock, } satisfies BlockSpecs; export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs); diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index cc9df706d..8b29844b3 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -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 */ diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 440a9b578..58a537815 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -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"; @@ -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). @@ -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, @@ -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), ]; }, diff --git a/packages/core/src/extensions/AIToolbar/AIToolbarPlugin.ts b/packages/core/src/extensions/AIToolbar/AIToolbarPlugin.ts new file mode 100644 index 000000000..9a9aaada5 --- /dev/null +++ b/packages/core/src/extensions/AIToolbar/AIToolbarPlugin.ts @@ -0,0 +1,167 @@ +import { Plugin, PluginKey, PluginView } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; + +import { BlockInfo, getBlockInfoFromPos } from "../../api/getBlockInfoFromPos"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; +import { EventEmitter } from "../../util/EventEmitter"; + +export type AIToolbarState = UiElementPosition & { prompt?: string }; + +export class AIToolbarView implements PluginView { + public state?: AIToolbarState; + public emitUpdate: () => void; + + public oldBlockInfo: BlockInfo | undefined; + public domElement: HTMLElement | undefined; + + constructor( + private readonly pmView: EditorView, + emitUpdate: (state: AIToolbarState) => void + ) { + this.emitUpdate = () => { + if (!this.state) { + throw new Error("Attempting to update uninitialized AI toolbar"); + } + + emitUpdate(this.state); + }; + + pmView.dom.addEventListener("dragstart", this.dragHandler); + pmView.dom.addEventListener("dragover", this.dragHandler); + pmView.dom.addEventListener("blur", this.blurHandler); + + // Setting capture=true ensures that any parent container of the editor that + // gets scrolled will trigger the scroll event. Scroll events do not bubble + // and so won't propagate to the document by default. + pmView.root.addEventListener("scroll", this.scrollHandler, true); + } + + blurHandler = (event: FocusEvent) => { + const editorWrapper = this.pmView.dom.parentElement!; + + // Checks if the focus is moving to an element outside the editor. If it is, + // the toolbar is hidden. + if ( + // An element is clicked. + event && + event.relatedTarget && + // Element is inside the editor. + (editorWrapper === (event.relatedTarget as Node) || + editorWrapper.contains(event.relatedTarget as Node) || + (event.relatedTarget as HTMLElement).matches( + ".bn-ui-container, .bn-ui-container *" + )) + ) { + return; + } + + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + }; + + // For dragging the whole editor. + dragHandler = () => { + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + }; + + scrollHandler = () => { + if (this.state?.show) { + this.state.referencePos = this.domElement!.getBoundingClientRect(); + this.emitUpdate(); + } + }; + + update(view: EditorView) { + const blockInfo = getBlockInfoFromPos( + view.state.doc, + view.state.selection.from + ); + + // Return if the selection remains in a non-AI block. + if ( + blockInfo.contentType.name !== "ai" && + this.oldBlockInfo?.contentType.name !== "ai" + ) { + this.oldBlockInfo = blockInfo; + return; + } + + this.oldBlockInfo = blockInfo; + + // Selection is in an AI block that wasn't previously selected. + if ( + blockInfo.contentType.name === "ai" && + blockInfo.contentNode.attrs.prompt !== "" && + view.state.selection.$from.sameParent(view.state.selection.$to) + ) { + this.domElement = view.domAtPos(blockInfo.startPos).node + .firstChild as HTMLElement; + + this.state = { + prompt: blockInfo.contentNode.attrs.prompt, + show: true, + referencePos: this.domElement.getBoundingClientRect(), + }; + + this.emitUpdate(); + + return; + } + + // Selection is not in an AI block but previously was in one. + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + } + + destroy() { + this.pmView.dom.removeEventListener("dragstart", this.dragHandler); + this.pmView.dom.removeEventListener("dragover", this.dragHandler); + this.pmView.dom.removeEventListener("blur", this.blurHandler); + + this.pmView.root.removeEventListener("scroll", this.scrollHandler, true); + } + + closeMenu = () => { + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + }; +} + +export const aiToolbarPluginKey = new PluginKey("AIToolbarPlugin"); + +export class AIToolbarProsemirrorPlugin extends EventEmitter { + private view: AIToolbarView | undefined; + public readonly plugin: Plugin; + + constructor() { + super(); + this.plugin = new Plugin({ + key: aiToolbarPluginKey, + view: (editorView) => { + this.view = new AIToolbarView(editorView, (state) => { + this.emit("update", state); + }); + return this.view; + }, + }); + } + + public get shown() { + return this.view?.state?.show || false; + } + + public onUpdate(callback: (state: AIToolbarState) => void) { + return this.on("update", callback); + } + + public closeMenu = () => this.view!.closeMenu(); +} diff --git a/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts b/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts index 35259d432..e34d6ba15 100644 --- a/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts +++ b/packages/core/src/extensions/Placeholder/PlaceholderPlugin.ts @@ -90,16 +90,17 @@ export const PlaceholderPlugin = ( const $pos = selection.$anchor; const node = $pos.parent; - - if (node.content.size > 0) { - return null; - } - const before = $pos.before(); - const dec = Decoration.node(before, before + node.nodeSize, { - "data-is-empty-and-focused": "true", - }); + const dec = Decoration.node( + before, + before + node.nodeSize, + node.content.size > 0 + ? { "data-is-focused": "true" } + : { + "data-is-empty-and-focused": "true", + } + ); return DecorationSet.create(doc, [dec]); }, diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 366430992..4e0123b90 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -271,6 +271,18 @@ export function getDefaultSlashMenuItems< }); } + if (checkDefaultBlockTypeInSchema("ai", editor)) { + items.push({ + onItemClick: () => { + insertOrUpdateBlock(editor, { + type: "ai", + }); + }, + key: "ai", + ...editor.dictionary.slash_menu.ai, + }); + } + items.push({ onItemClick: () => editor.openSelectionMenu(":"), key: "emoji", diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 7c81fd8e7..543bc62e5 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -96,6 +96,12 @@ export const ar: Dictionary = { aliases: ["رمز تعبيري", "إيموجي", "إيموت", "عاطفة", "وجه"], group: "آخرون", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "أدخل نصًا أو اكتب '/' للأوامر", @@ -103,6 +109,7 @@ export const ar: Dictionary = { bulletListItem: "قائمة", numberedListItem: "قائمة", checkListItem: "قائمة", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -288,6 +295,11 @@ export const ar: Dictionary = { url_placeholder: "تحرير الرابط", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index b7dc7ac43..64f0ebf4a 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -110,6 +110,12 @@ export const en = { aliases: ["emoji", "emote", "emotion", "face"], group: "Others", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Enter text or type '/' for commands", @@ -117,6 +123,7 @@ export const en = { bulletListItem: "List", numberedListItem: "List", checkListItem: "List", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -302,6 +309,11 @@ export const en = { url_placeholder: "Edit URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index bd9e073aa..581f09c30 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -111,6 +111,12 @@ export const fr: Dictionary = { aliases: ["emoji", "émoticône", "émotion", "visage"], group: "Autres", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Entrez du texte ou tapez '/' pour les commandes", @@ -118,6 +124,7 @@ export const fr: Dictionary = { bulletListItem: "Liste", numberedListItem: "Liste", checkListItem: "Liste", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -303,6 +310,11 @@ export const fr: Dictionary = { url_placeholder: "Modifier l'URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index 4b4ee544d..739e1e014 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -104,6 +104,12 @@ export const is: Dictionary = { aliases: ["emoji", "andlitsávísun", "tilfinningar", "andlit"], group: "Annað", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Sláðu inn texta eða skrifaðu '/' fyrir skipanir", @@ -111,6 +117,7 @@ export const is: Dictionary = { bulletListItem: "Listi", numberedListItem: "Listi", checkListItem: "Listi", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -295,6 +302,11 @@ export const is: Dictionary = { url_placeholder: "Breyta URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index 91c7d983d..d5999a541 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -131,6 +131,12 @@ export const ja: Dictionary = { aliases: ["絵文字", "顔文字", "感情表現", "顔"], group: "その他", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "テキストを入力するか'/' を入力してコマンド選択", @@ -138,6 +144,7 @@ export const ja: Dictionary = { bulletListItem: "リストを追加", numberedListItem: "リストを追加", checkListItem: "リストを追加", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -323,6 +330,11 @@ export const ja: Dictionary = { url_placeholder: "URLを編集", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 9c7f6bf92..4c31db60d 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -124,6 +124,12 @@ export const ko: Dictionary = { ], group: "기타", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "텍스트를 입력하거나 /를 입력하여 명령을 입력하세요.", @@ -131,6 +137,7 @@ export const ko: Dictionary = { bulletListItem: "목록", numberedListItem: "목록", checkListItem: "목록", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -316,6 +323,11 @@ export const ko: Dictionary = { url_placeholder: "URL 수정", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index 782645c1a..cd6ec5bb0 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -111,6 +111,12 @@ export const nl: Dictionary = { ], group: "Overig", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Voer tekst in of type '/' voor commando's", @@ -118,6 +124,7 @@ export const nl: Dictionary = { bulletListItem: "Lijst", numberedListItem: "Lijst", checkListItem: "Lijst", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -302,6 +309,11 @@ export const nl: Dictionary = { url_placeholder: "Bewerk URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index 89aedc0a5..bb3159fc6 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -96,6 +96,12 @@ export const pl: Dictionary = { aliases: ["emoji", "emotka", "wyrażenie emocji", "twarz"], group: "Inne", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Wprowadź tekst lub wpisz '/' aby użyć poleceń", @@ -103,6 +109,7 @@ export const pl: Dictionary = { bulletListItem: "Lista", numberedListItem: "Lista", checkListItem: "Lista", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -287,6 +294,11 @@ export const pl: Dictionary = { url_placeholder: "Edytuj URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 6ee76d166..2877c64c9 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -103,6 +103,12 @@ export const pt: Dictionary = { aliases: ["emoji", "emoticon", "expressão emocional", "rosto"], group: "Outros", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Digite texto ou use '/' para comandos", @@ -110,6 +116,7 @@ export const pt: Dictionary = { bulletListItem: "Lista", numberedListItem: "Lista", checkListItem: "Lista", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -295,6 +302,11 @@ export const pt: Dictionary = { url_placeholder: "Editar URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index 51a8f2653..0b2869d1c 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -138,6 +138,12 @@ export const ru: Dictionary = { aliases: ["эмодзи", "смайлик", "выражение эмоций", "лицо"], group: "Прочее", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Ведите текст или введите «/» для команд", @@ -145,6 +151,7 @@ export const ru: Dictionary = { bulletListItem: "Список", numberedListItem: "Список", checkListItem: "Список", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -330,6 +337,11 @@ export const ru: Dictionary = { url_placeholder: "Изменить URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index f188196dd..e075bf6da 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -110,6 +110,12 @@ export const vi: Dictionary = { ], group: "Khác", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "Nhập văn bản hoặc gõ '/' để thêm định dạng", @@ -117,6 +123,7 @@ export const vi: Dictionary = { bulletListItem: "Danh sách", numberedListItem: "Danh sách", checkListItem: "Danh sách", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -302,6 +309,11 @@ export const vi: Dictionary = { url_placeholder: "Chỉnh sửa URL", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index 6c2835659..dccc5faf3 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -144,6 +144,12 @@ export const zh: Dictionary = { ], group: "其他", }, + ai: { + title: "AI Block", + subtext: "Create content using generative AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "Others", + }, }, placeholders: { default: "输入 '/' 以使用命令", @@ -151,6 +157,7 @@ export const zh: Dictionary = { bulletListItem: "列表", numberedListItem: "列表", checkListItem: "列表", + ai: "Enter a prompt", }, file_blocks: { image: { @@ -336,6 +343,11 @@ export const zh: Dictionary = { url_placeholder: "编辑链接地址", }, }, + ai_toolbar: { + show_prompt: "Show prompt", + update: "Update", + updating: "Updating…", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ad4c657b..5d626244c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,7 @@ import * as locales from "./i18n/locales"; export * from "./api/exporters/html/externalHTMLExporter"; export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testUtil"; +export * from "./blocks/AIBlockContent/AIBlockContent"; export * from "./blocks/AudioBlockContent/AudioBlockContent"; export * from "./blocks/FileBlockContent/FileBlockContent"; export * from "./blocks/ImageBlockContent/ImageBlockContent"; diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index 94c423ff2..ad9ab55d3 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -55,6 +55,10 @@ export * from "./BlockNoteTheme"; export * from "./defaultThemes"; export const components: Components = { + AIToolbar: { + Root: Toolbar, + Button: ToolbarButton, + }, FormattingToolbar: { Root: Toolbar, Button: ToolbarButton, diff --git a/packages/mantine/src/toolbar/ToolbarButton.tsx b/packages/mantine/src/toolbar/ToolbarButton.tsx index bd22f0411..324ceb31b 100644 --- a/packages/mantine/src/toolbar/ToolbarButton.tsx +++ b/packages/mantine/src/toolbar/ToolbarButton.tsx @@ -51,10 +51,12 @@ export const ToolbarButton = forwardRef( + mainTooltip && ( + + ) }> {/*Creates an ActionIcon instead of a Button if only an icon is provided as content.*/} {children ? ( @@ -72,8 +74,9 @@ export const ToolbarButton = forwardRef( aria-pressed={isSelected} data-selected={isSelected || undefined} data-test={ + mainTooltip && mainTooltip.slice(0, 1).toLowerCase() + - mainTooltip.replace(/\s+/g, "").slice(1) + mainTooltip.replace(/\s+/g, "").slice(1) } size={"xs"} disabled={isDisabled || false} @@ -96,8 +99,9 @@ export const ToolbarButton = forwardRef( aria-pressed={isSelected} data-selected={isSelected || undefined} data-test={ + mainTooltip && mainTooltip.slice(0, 1).toLowerCase() + - mainTooltip.replace(/\s+/g, "").slice(1) + mainTooltip.replace(/\s+/g, "").slice(1) } size={30} disabled={isDisabled || false} diff --git a/packages/react/src/components/AIToolbar/AIToolbar.tsx b/packages/react/src/components/AIToolbar/AIToolbar.tsx new file mode 100644 index 000000000..b4b348932 --- /dev/null +++ b/packages/react/src/components/AIToolbar/AIToolbar.tsx @@ -0,0 +1,37 @@ +import { ReactNode, useState } from "react"; + +import { useComponentsContext } from "../../editor/ComponentsContext"; +import { AIToolbarProps } from "./AIToolbarProps"; +import { ShowPromptButton } from "./DefaultButtons/ShowPromptButton"; +import { UpdateButton } from "./DefaultButtons/UpdateButton"; + +export const getAIToolbarItems = ( + props: AIToolbarProps & { + updating: boolean; + setUpdating: (updating: boolean) => void; + } +): JSX.Element[] => [ + , + , +]; + +/** + * By default, the AIToolbar component will render with default buttons. + * However, you can override the selects/buttons to render by passing children. + * The children you pass should be: + * + * - Default buttons: Components found within the `/DefaultButtons` directory. + * - Custom buttons: Buttons made using the Components.AIToolbar.Button + * component from the component context. + */ +export const AIToolbar = (props: AIToolbarProps & { children?: ReactNode }) => { + const Components = useComponentsContext()!; + + const [updating, setUpdating] = useState(false); + + return ( + + {props.children || getAIToolbarItems({ ...props, updating, setUpdating })} + + ); +}; diff --git a/packages/react/src/components/AIToolbar/AIToolbarController.tsx b/packages/react/src/components/AIToolbar/AIToolbarController.tsx new file mode 100644 index 000000000..f6e665cdf --- /dev/null +++ b/packages/react/src/components/AIToolbar/AIToolbarController.tsx @@ -0,0 +1,59 @@ +import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { flip, offset } from "@floating-ui/react"; +import { FC } from "react"; + +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; +import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { AIToolbar } from "./AIToolbar"; +import { AIToolbarProps } from "./AIToolbarProps"; + +export const AIToolbarController = (props: { + aiToolbar?: FC; +}) => { + const editor = useBlockNoteEditor< + BlockSchema, + InlineContentSchema, + StyleSchema + >(); + + if (!editor.aiToolbar) { + throw new Error( + "AIToolbarController can only be used when BlockNote editor schema contains an AI block" + ); + } + + const state = useUIPluginState( + editor.aiToolbar.onUpdate.bind(editor.aiToolbar) + ); + + const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning( + state?.show || false, + state?.referencePos || null, + 3000, + { + placement: "top-end", + middleware: [offset(10), flip()], + onOpenChange: (open) => { + if (!open) { + editor.linkToolbar.closeMenu(); + editor.focus(); + } + }, + } + ); + + if (!isMounted || !state) { + return null; + } + + const { prompt } = state; + + const Component = props.aiToolbar || AIToolbar; + + return ( +
+ +
+ ); +}; diff --git a/packages/react/src/components/AIToolbar/AIToolbarProps.ts b/packages/react/src/components/AIToolbar/AIToolbarProps.ts new file mode 100644 index 000000000..cfcc7086c --- /dev/null +++ b/packages/react/src/components/AIToolbar/AIToolbarProps.ts @@ -0,0 +1,3 @@ +export type AIToolbarProps = { + prompt: string; +}; diff --git a/packages/react/src/components/AIToolbar/DefaultButtons/ShowPromptButton.tsx b/packages/react/src/components/AIToolbar/DefaultButtons/ShowPromptButton.tsx new file mode 100644 index 000000000..9e1b86c66 --- /dev/null +++ b/packages/react/src/components/AIToolbar/DefaultButtons/ShowPromptButton.tsx @@ -0,0 +1,102 @@ +import { + aiBlockConfig, + BlockSchemaWithBlock, + InlineContentSchema, + mockAIModelCall, + StyleSchema, +} from "@blocknote/core"; +import { + ChangeEvent, + KeyboardEvent, + useCallback, + useEffect, + useState, +} from "react"; +import { RiSparkling2Fill } from "react-icons/ri"; + +import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../i18n/dictionary"; +import { AIToolbarProps } from "../AIToolbarProps"; + +export const ShowPromptButton = ( + props: AIToolbarProps & { + setUpdating: (updating: boolean) => void; + } +) => { + const dict = useDictionary(); + const Components = useComponentsContext()!; + + const editor = useBlockNoteEditor< + BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, + InlineContentSchema, + StyleSchema + >(); + + const [opened, setOpened] = useState(false); + const [currentEditingPrompt, setCurrentEditingPrompt] = useState( + props.prompt + ); + + const handleClick = useCallback(() => setOpened(!opened), [opened]); + + const handleEnter = useCallback( + async (event: KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); + props.setUpdating(true); + setOpened(false); + editor.updateBlock(editor.getTextCursorPosition().block, { + props: { prompt: props.prompt }, + content: await mockAIModelCall(props.prompt), + }); + props.setUpdating(false); + } + }, + [editor, props] + ); + + const handleChange = useCallback( + (event: ChangeEvent) => + setCurrentEditingPrompt(event.currentTarget.value), + [] + ); + + useEffect(() => { + const callback = () => setOpened(false); + + editor.domElement.addEventListener("mousedown", callback); + + return () => { + editor.domElement.removeEventListener("mousedown", callback); + }; + }, [editor.domElement]); + + return ( + + + + {dict.ai_toolbar.show_prompt} + + + + + } + value={currentEditingPrompt || ""} + autoFocus={true} + placeholder={""} + onKeyDown={handleEnter} + onChange={handleChange} + /> + + + + ); +}; diff --git a/packages/react/src/components/AIToolbar/DefaultButtons/UpdateButton.tsx b/packages/react/src/components/AIToolbar/DefaultButtons/UpdateButton.tsx new file mode 100644 index 000000000..0a22e9f85 --- /dev/null +++ b/packages/react/src/components/AIToolbar/DefaultButtons/UpdateButton.tsx @@ -0,0 +1,49 @@ +import { + aiBlockConfig, + BlockSchemaWithBlock, + InlineContentSchema, + mockAIModelCall, + StyleSchema, +} from "@blocknote/core"; + +import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../i18n/dictionary"; +import { AIToolbarProps } from "../AIToolbarProps"; + +export const UpdateButton = ( + props: AIToolbarProps & { + updating: boolean; + setUpdating: (updating: boolean) => void; + } +) => { + const dict = useDictionary(); + const Components = useComponentsContext()!; + + const editor = useBlockNoteEditor< + BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, + InlineContentSchema, + StyleSchema + >(); + + if (!editor.isEditable) { + return null; + } + + return ( + { + props.setUpdating(true); + editor.focus(); + editor.updateBlock(editor.getTextCursorPosition().block, { + props: { prompt: props.prompt }, + content: await mockAIModelCall(props.prompt), + }); + props.setUpdating(false); + }}> + {props.updating ? dict.ai_toolbar.updating : dict.ai_toolbar.update} + + ); +}; diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index 7b02f5279..1f4b90d59 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -14,6 +14,7 @@ import { RiListCheck3, RiListOrdered, RiListUnordered, + RiSparkling2Fill, RiText, } from "react-icons/ri"; @@ -93,6 +94,12 @@ export const blockTypeSelectItems = ( icon: RiListCheck3, isSelected: (block) => block.type === "checkListItem", }, + { + name: dict.slash_menu.ai.title, + type: "ai", + icon: RiSparkling2Fill, + isSelected: (block) => block.type === "ai", + }, ]; export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { diff --git a/packages/react/src/components/SideMenu/SideMenu.tsx b/packages/react/src/components/SideMenu/SideMenu.tsx index 1331773c5..90aa21d1a 100644 --- a/packages/react/src/components/SideMenu/SideMenu.tsx +++ b/packages/react/src/components/SideMenu/SideMenu.tsx @@ -50,6 +50,10 @@ export const SideMenu = < } } + if (props.block.type === "ai" && props.block.props.prompt) { + attrs["data-prompt"] = props.block.props.prompt.toString(); + } + return attrs; }, [props.block, props.editor.schema.blockSchema]); diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx index e17a24759..d991da10b 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx @@ -11,13 +11,14 @@ import { RiH2, RiH3, RiFile2Line, + RiFilmLine, RiImage2Fill, RiListCheck3, RiListOrdered, RiListUnordered, + RiSparkling2Fill, RiTable2, RiText, - RiFilmLine, RiVolumeUpFill, } from "react-icons/ri"; import { DefaultReactSuggestionItem } from "./types"; @@ -35,6 +36,7 @@ const icons = { video: RiFilmLine, audio: RiVolumeUpFill, file: RiFile2Line, + ai: RiSparkling2Fill, emoji: RiEmotionFill, }; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 237c843bb..8de98ef59 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -6,6 +6,7 @@ import { SuggestionMenuController } from "../components/SuggestionMenu/Suggestio import { GridSuggestionMenuController } from "../components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; +import { AIToolbarController } from "../components/AIToolbar/AIToolbarController"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -15,6 +16,7 @@ export type BlockNoteDefaultUIProps = { filePanel?: boolean; tableHandles?: boolean; emojiPicker?: boolean; + aiToolbar?: boolean; }; export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { @@ -45,6 +47,7 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { {editor.tableHandles && props.tableHandles !== false && ( )} + {editor.aiToolbar && props.aiToolbar !== false && } ); } diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index 7a46df454..99744ae8d 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -13,6 +13,24 @@ import { DefaultReactSuggestionItem } from "../components/SuggestionMenu/types"; import { DefaultReactGridSuggestionItem } from "../components/SuggestionMenu/GridSuggestionMenu/types"; export type ComponentProps = { + AIToolbar: { + Root: { + className?: string; + children?: ReactNode; + }; + Button: { + className?: string; + mainTooltip?: string; + secondaryTooltip?: string; + icon?: ReactNode; + onClick?: (e: MouseEvent) => void; + isSelected?: boolean; + isDisabled?: boolean; + } & ( + | { children: ReactNode; label?: string } + | { children?: undefined; label: string } + ); + }; FormattingToolbar: { Root: { className?: string; @@ -20,7 +38,7 @@ export type ComponentProps = { }; Button: { className?: string; - mainTooltip: string; + mainTooltip?: string; secondaryTooltip?: string; icon?: ReactNode; onClick?: (e: MouseEvent) => void; @@ -89,7 +107,7 @@ export type ComponentProps = { }; Button: { className?: string; - mainTooltip: string; + mainTooltip?: string; secondaryTooltip?: string; icon?: ReactNode; onClick?: (e: MouseEvent) => void; diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css index 2ac35161d..2fece0e28 100644 --- a/packages/react/src/editor/styles.css +++ b/packages/react/src/editor/styles.css @@ -238,3 +238,7 @@ .bn-side-menu[data-url="false"] { height: 54px; } + +.bn-side-menu[data-block-type="ai"]:not([data-prompt]) { + height: 58px; +} diff --git a/packages/shadcn/src/index.tsx b/packages/shadcn/src/index.tsx index cb40f75d0..506ef1f18 100644 --- a/packages/shadcn/src/index.tsx +++ b/packages/shadcn/src/index.tsx @@ -49,6 +49,10 @@ import { Toolbar, ToolbarButton, ToolbarSelect } from "./toolbar/Toolbar"; import "./style.css"; export const components: Components = { + AIToolbar: { + Root: Toolbar, + Button: ToolbarButton, + }, FormattingToolbar: { Root: Toolbar, Button: ToolbarButton, diff --git a/packages/shadcn/src/toolbar/Toolbar.tsx b/packages/shadcn/src/toolbar/Toolbar.tsx index f5d4aaac6..2f8649a67 100644 --- a/packages/shadcn/src/toolbar/Toolbar.tsx +++ b/packages/shadcn/src/toolbar/Toolbar.tsx @@ -89,18 +89,22 @@ export const ToolbarButton = forwardRef( ); - return ( - - - {trigger} - - - {mainTooltip} - {secondaryTooltip && {secondaryTooltip}} - - - ); + if (mainTooltip) { + return ( + + + {trigger} + + + {mainTooltip} + {secondaryTooltip && {secondaryTooltip}} + + + ); + } + + return trigger; } ); From 0da498edd336c1c81bd49c7ef4e41c536c71482c Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 6 Aug 2024 22:05:11 +0200 Subject: [PATCH 02/45] Added inline and slash menu AI --- package-lock.json | 1 + packages/ariakit/src/index.tsx | 6 +- .../blocks/AIBlockContent/AIBlockContent.ts | 21 +- .../blocks/AIBlockContent/mockAIOperation.ts | 263 ++++++++++++++++++ packages/core/src/editor/Block.css | 2 + packages/core/src/editor/BlockNoteEditor.ts | 13 +- .../AIBlockToolbarPlugin.ts} | 20 +- .../AIInlineToolbar/AIInlineToolbarPlugin.ts | 229 +++++++++++++++ .../getDefaultSlashMenuItems.ts | 15 +- packages/core/src/i18n/locales/ar.ts | 14 +- packages/core/src/i18n/locales/en.ts | 39 ++- packages/core/src/i18n/locales/fr.ts | 14 +- packages/core/src/i18n/locales/is.ts | 14 +- packages/core/src/i18n/locales/ja.ts | 14 +- packages/core/src/i18n/locales/ko.ts | 14 +- packages/core/src/i18n/locales/nl.ts | 14 +- packages/core/src/i18n/locales/pl.ts | 14 +- packages/core/src/i18n/locales/pt.ts | 14 +- packages/core/src/i18n/locales/ru.ts | 14 +- packages/core/src/i18n/locales/vi.ts | 14 +- packages/core/src/i18n/locales/zh.ts | 14 +- packages/core/src/index.ts | 2 + packages/mantine/src/index.tsx | 6 +- packages/react/package.json | 1 + .../AIBlockToolbar.tsx} | 17 +- .../AIBlockToolbarController.tsx} | 14 +- .../AIBlockToolbar/AIBlockToolbarProps.ts | 3 + .../DefaultButtons/ShowPromptButton.tsx | 25 +- .../DefaultButtons/UpdateButton.tsx | 23 +- .../AIInlineToolbar/AIInlineToolbar.tsx | 42 +++ .../AIInlineToolbarController.tsx | 53 ++++ .../AIInlineToolbar/AIInlineToolbarProps.ts | 6 + .../DefaultButtons/AcceptButton.tsx | 38 +++ .../DefaultButtons/RetryButton.tsx | 80 ++++++ .../DefaultButtons/RevertButton.tsx | 64 +++++ .../components/AIToolbar/AIToolbarProps.ts | 3 - .../DefaultButtons/AIButton.tsx | 116 ++++++++ .../DefaultSelects/BlockTypeSelect.tsx | 2 +- .../FormattingToolbar/FormattingToolbar.tsx | 2 + .../SuggestionMenu/getDefaultAIMenuItems.tsx | 61 ++++ .../getDefaultReactSlashMenuItems.tsx | 1 + .../react/src/editor/BlockNoteDefaultUI.tsx | 22 +- .../react/src/editor/ComponentsContext.tsx | 20 +- packages/shadcn/src/index.tsx | 6 +- 44 files changed, 1271 insertions(+), 99 deletions(-) create mode 100644 packages/core/src/blocks/AIBlockContent/mockAIOperation.ts rename packages/core/src/extensions/{AIToolbar/AIToolbarPlugin.ts => AIBlockToolbar/AIBlockToolbarPlugin.ts} (87%) create mode 100644 packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts rename packages/react/src/components/{AIToolbar/AIToolbar.tsx => AIBlockToolbar/AIBlockToolbar.tsx} (69%) rename packages/react/src/components/{AIToolbar/AIToolbarController.tsx => AIBlockToolbar/AIBlockToolbarController.tsx} (77%) create mode 100644 packages/react/src/components/AIBlockToolbar/AIBlockToolbarProps.ts rename packages/react/src/components/{AIToolbar => AIBlockToolbar}/DefaultButtons/ShowPromptButton.tsx (81%) rename packages/react/src/components/{AIToolbar => AIBlockToolbar}/DefaultButtons/UpdateButton.tsx (63%) create mode 100644 packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx create mode 100644 packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx create mode 100644 packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts create mode 100644 packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx create mode 100644 packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx create mode 100644 packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx delete mode 100644 packages/react/src/components/AIToolbar/AIToolbarProps.ts create mode 100644 packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx create mode 100644 packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx diff --git a/package-lock.json b/package-lock.json index f9e2eb0a3..3e975edb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28272,6 +28272,7 @@ "@blocknote/core": "^0.15.3", "@floating-ui/react": "^0.26.4", "@tiptap/core": "^2.5.0", + "@tiptap/pm": "2.5.0", "@tiptap/react": "^2.5.0", "lodash.merge": "^4.6.2", "react": "^18", diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx index 880bf2199..350be21bf 100644 --- a/packages/ariakit/src/index.tsx +++ b/packages/ariakit/src/index.tsx @@ -46,7 +46,11 @@ import { ToolbarSelect } from "./toolbar/ToolbarSelect"; import "./style.css"; export const components: Components = { - AIToolbar: { + AIBlockToolbar: { + Root: Toolbar, + Button: ToolbarButton, + }, + AIInlineToolbar: { Root: Toolbar, Button: ToolbarButton, }, diff --git a/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts b/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts index 55c32a772..53f7adc38 100644 --- a/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts +++ b/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts @@ -6,16 +6,7 @@ import { PropSchema, } from "../../schema"; import { defaultProps } from "../defaultProps"; - -export const mockAIModelCall = async (_prompt: string) => { - return new Promise((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); - }); -}; +import { mockAIOperation } from "./mockAIOperation"; export const aiPropSchema = { ...defaultProps, @@ -36,12 +27,18 @@ export const aiRender = ( ) => { if (!block.props.prompt) { const generateResponseCallback = async () => { + const prompt = span.innerText; generateButton.textContent = "Generating..."; + const response = await mockAIOperation(editor, prompt, { + operation: "replaceBlock", + blockIdentifier: block.id, + }); + editor.updateBlock(block, { type: "ai", - props: { prompt: span.innerText }, - content: await mockAIModelCall(block.props.prompt), + props: { prompt }, + content: response[0].content, }); }; diff --git a/packages/core/src/blocks/AIBlockContent/mockAIOperation.ts b/packages/core/src/blocks/AIBlockContent/mockAIOperation.ts new file mode 100644 index 000000000..8eb25293d --- /dev/null +++ b/packages/core/src/blocks/AIBlockContent/mockAIOperation.ts @@ -0,0 +1,263 @@ +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { BlockIdentifier } from "../../schema"; + +// Mock function to edit the document using AI. Has 3 operation modes: +// - replaceSelection: Replaces the selected text with AI generated text. To +// make things easier, this actually replaces the selected blocks, and the final +// prompt specifies to only modify the selected text. +// - replaceBlock: Replaces a block with AI generated content. The block has to +// be specified when calling the function. +// - insert: Inserts AI generated text at the text cursor position. Assumes that +// a selection isn't active. To make things easier, this actually replaces the +// block containing the text cursor, and the final prompt specifies to only +// append content to it. +export const mockAIOperation = async ( + editor: BlockNoteEditor, + prompt: string, + options: + | { operation: "replaceSelection" } + | { operation: "replaceBlock"; blockIdentifier: BlockIdentifier } + | { operation: "insert" } = { operation: "insert" } +) => { + const document = editor.document; + + // Un-nests blocks, as we're using Markdown to convert the blocks to a prompt + // for the AI model anyway. + let noNestedBlocks = false; + while (!noNestedBlocks) { + noNestedBlocks = true; + + for (let i = document.length - 1; i >= 0; i--) { + const children = document[i].children; + + if (children.length !== 0) { + noNestedBlocks = false; + document[i].children = []; + document.splice(i + 1, 0, ...children); + } + } + } + + let fullPrompt: string; + + if (options.operation === "replaceSelection") { + const selectedBlocks = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + + const firstSelectedBlockIndex = document.findIndex( + (block) => block.id === selectedBlocks[0].id + ); + const lastSelectedBlockIndex = document.findIndex( + (block) => block.id === selectedBlocks[selectedBlocks.length - 1].id + ); + + // We split the document to provide the AI model with full context of what + // exactly is selected. + const before = document.slice(0, firstSelectedBlockIndex); + const selected = document.slice( + firstSelectedBlockIndex, + lastSelectedBlockIndex + 1 + ); + const after = document.slice(lastSelectedBlockIndex + 1); + + const beforeText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.$from.start(), + editor._tiptapEditor.state.selection.from + ); + const selectedText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.from, + editor._tiptapEditor.state.selection.to + ); + const afterText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.to, + editor._tiptapEditor.state.selection.$to.end() + ); + + const beforeMarkdown = await editor.blocksToMarkdownLossy(before); + const selectedMarkdown = await editor.blocksToMarkdownLossy(selected); + const afterMarkdown = await editor.blocksToMarkdownLossy(after); + + fullPrompt = `Below is the content of an editable Markdown document. It's split into sections to show where the user selection is located. + +Content before: +\`\`\` +${beforeMarkdown} +\`\`\` +The Markdown content in the document before the selected section. + +Selected section: +\`\`\` +${selectedMarkdown} +\`\`\` +The Markdown content of the selected section. + +Content after: +\`\`\` +${afterMarkdown} +\`\`\` +The Markdown content in the document after the selected section. + +Text before: +\`\`\` +${beforeText} +\`\`\` +The text between the start of the selected section and the start of the user selection. + +Selected text: +\`\`\` +${selectedText} +\`\`\` +The user selected text within the selected section. + +Text after: +\`\`\` +${afterText} +\`\`\` +The text between the end of the user selection and the end of the selected section. + +Prompt: +\`\`\` +${prompt} +\`\`\` +The AI prompt provided by the user. + +Provide a version of the selected section where the selected text inside is modified based on the prompt. Provide Markdown only, the response should not contain any additional text. +`; + } else if (options.operation === "replaceBlock") { + const blockID = + typeof options.blockIdentifier === "string" + ? options.blockIdentifier + : options.blockIdentifier.id; + + const selectedBlockIndex = document.findIndex( + (block) => block.id === blockID + ); + + // We split the document to provide the AI model with full context of what + // exactly is selected. + const before = document.slice(0, selectedBlockIndex); + const selected = document[selectedBlockIndex]; + const after = document.slice(selectedBlockIndex + 1); + + const beforeMarkdown = await editor.blocksToMarkdownLossy(before); + const selectedMarkdown = await editor.blocksToMarkdownLossy([selected]); + const afterMarkdown = await editor.blocksToMarkdownLossy(after); + + fullPrompt = `Below is the content of an editable Markdown document. It's split into sections to show where the user selection is located. + +Content before: +\`\`\` +${beforeMarkdown} +\`\`\` +The Markdown content in the document before the selected section. + +Selected section: +\`\`\` +${selectedMarkdown} +\`\`\` +The Markdown content of the selected section. + +Content after: +\`\`\` +${afterMarkdown} +\`\`\` +The Markdown content in the document after the selected section. + +Prompt: +\`\`\` +${prompt} +\`\`\` +The AI prompt provided by the user. + +Provide a version of the selected section which is modified based on the prompt. Provide Markdown only, the response should not contain any additional text. +`; + } else { + const selectedBlock = editor.getTextCursorPosition().block; + + const selectedBlockIndex = document.findIndex( + (block) => block.id === selectedBlock.id + ); + + // We split the document to provide the AI model with full context of what + // exactly is selected. + const before = document.slice(0, selectedBlockIndex); + const selected = document[selectedBlockIndex]; + const after = document.slice(selectedBlockIndex + 1); + + const beforeText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.$from.start(), + editor._tiptapEditor.state.selection.from + ); + const afterText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.to, + editor._tiptapEditor.state.selection.$to.end() + ); + + const beforeMarkdown = await editor.blocksToMarkdownLossy(before); + const selectedMarkdown = await editor.blocksToMarkdownLossy([selected]); + const afterMarkdown = await editor.blocksToMarkdownLossy(after); + + fullPrompt = `Below is the content of an editable Markdown document. It's split into sections to show where the user selection is located. + +Content before: +\`\`\` +${beforeMarkdown} +\`\`\` +The Markdown content in the document before the selected section. + +Selected section: +\`\`\` +${selectedMarkdown} +\`\`\` +The Markdown content of the selected section. + +Content after: +\`\`\` +${afterMarkdown} +\`\`\` +The Markdown content in the document after the selected section. + +Text before: +\`\`\` +${beforeText} +\`\`\` +The text between the start of the selected section and the text cursor. + +Text after: +\`\`\` +${afterText} +\`\`\` +The text between the text cursor and the end of the selected section. + +Prompt: +\`\`\` +${prompt} +\`\`\` +The AI prompt provided by the user. + +Provide a modified version of the selected section where content based on the prompt is inserted at the text cursor position. Provide Markdown only, the response should not contain any additional text. +`; + } + + console.log(fullPrompt); + + const mockFetchAIResponse = async (_prompt: string) => { + return new Promise((resolve) => { + setTimeout(() => { + // resolve( + // `The text before this block discusses cats, focusing on their domestication and origins in the Near East around 7500 BC. It highlights their classification as a domesticated species within the family Felidae. + // + // The text after this block describes dogs, noting their descent from wolves and their domestication over 14,000 years ago. It emphasizes that dogs were the first species domesticated by humans.` + // ); + resolve( + 'Over time, a variety of different cat breeds have emerged. One of these is the Maine Coon, which is one of the largest domesticated cat breeds. Known for their friendly and sociable nature, Maine Coons are often referred to as "gentle giants." They have a distinctive physical appearance, characterized by their tufted ears, bushy tails, and long, shaggy fur that is well-suited for cold climates. Maine Coons are also intelligent and playful, making them excellent companions for families.\n' + ); + }, 1000); + }); + }; + + const aiResponse = await mockFetchAIResponse(fullPrompt); + + return await editor.tryParseMarkdownToBlocks(aiResponse); +}; diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 8b29844b3..be43553a7 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -407,10 +407,12 @@ NESTED BLOCKS border-radius: 4px; } +.bn-editor[contenteditable="true"] [data-content-type="ai"][data-prompt] p:hover { outline: solid 3px rgba(154, 56, 173, 0.1); } +.bn-editor[contenteditable="true"] [data-content-type="ai"][data-prompt][data-is-focused] p { outline: solid 3px rgba(154, 56, 173, 0.2); } diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 58a537815..c44a18c86 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -26,7 +26,8 @@ import { DefaultStyleSchema, PartialBlock, } from "../blocks/defaultBlocks"; -import { AIToolbarProsemirrorPlugin } from "../extensions/AIToolbar/AIToolbarPlugin"; +import { AIBlockToolbarProsemirrorPlugin } from "../extensions/AIBlockToolbar/AIBlockToolbarPlugin"; +import { AIInlineToolbarProsemirrorPlugin } from "../extensions/AIInlineToolbar/AIInlineToolbarPlugin"; import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin"; import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin"; import { LinkToolbarProsemirrorPlugin } from "../extensions/LinkToolbar/LinkToolbarPlugin"; @@ -244,7 +245,8 @@ export class BlockNoteEditor< ISchema, SSchema >; - public readonly aiToolbar?: AIToolbarProsemirrorPlugin; + public readonly aiBlockToolbar?: AIBlockToolbarProsemirrorPlugin; + public readonly aiInlineToolbar: AIInlineToolbarProsemirrorPlugin; /** * The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload). @@ -331,9 +333,11 @@ export class BlockNoteEditor< this.tableHandles = new TableHandlesProsemirrorPlugin(this as any); } if (checkDefaultBlockTypeInSchema("ai", this)) { - this.aiToolbar = new AIToolbarProsemirrorPlugin(); + this.aiBlockToolbar = new AIBlockToolbarProsemirrorPlugin(); } + this.aiInlineToolbar = new AIInlineToolbarProsemirrorPlugin(); + const extensions = getBlockNoteExtensions({ editor: this, domAttributes: newOptions.domAttributes || {}, @@ -356,7 +360,8 @@ export class BlockNoteEditor< this.suggestionMenus.plugin, ...(this.filePanel ? [this.filePanel.plugin] : []), ...(this.tableHandles ? [this.tableHandles.plugin] : []), - ...(this.aiToolbar ? [this.aiToolbar.plugin] : []), + ...(this.aiBlockToolbar ? [this.aiBlockToolbar.plugin] : []), + this.aiInlineToolbar.plugin, PlaceholderPlugin(this, newOptions.placeholders), ]; }, diff --git a/packages/core/src/extensions/AIToolbar/AIToolbarPlugin.ts b/packages/core/src/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts similarity index 87% rename from packages/core/src/extensions/AIToolbar/AIToolbarPlugin.ts rename to packages/core/src/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts index 9a9aaada5..d15adcd7c 100644 --- a/packages/core/src/extensions/AIToolbar/AIToolbarPlugin.ts +++ b/packages/core/src/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts @@ -5,10 +5,10 @@ import { BlockInfo, getBlockInfoFromPos } from "../../api/getBlockInfoFromPos"; import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; -export type AIToolbarState = UiElementPosition & { prompt?: string }; +export type AIBlockToolbarState = UiElementPosition & { prompt?: string }; -export class AIToolbarView implements PluginView { - public state?: AIToolbarState; +export class AIBlockToolbarView implements PluginView { + public state?: AIBlockToolbarState; public emitUpdate: () => void; public oldBlockInfo: BlockInfo | undefined; @@ -16,7 +16,7 @@ export class AIToolbarView implements PluginView { constructor( private readonly pmView: EditorView, - emitUpdate: (state: AIToolbarState) => void + emitUpdate: (state: AIBlockToolbarState) => void ) { this.emitUpdate = () => { if (!this.state) { @@ -136,18 +136,18 @@ export class AIToolbarView implements PluginView { }; } -export const aiToolbarPluginKey = new PluginKey("AIToolbarPlugin"); +export const aiBlockToolbarPluginKey = new PluginKey("AIBlockToolbarPlugin"); -export class AIToolbarProsemirrorPlugin extends EventEmitter { - private view: AIToolbarView | undefined; +export class AIBlockToolbarProsemirrorPlugin extends EventEmitter { + private view: AIBlockToolbarView | undefined; public readonly plugin: Plugin; constructor() { super(); this.plugin = new Plugin({ - key: aiToolbarPluginKey, + key: aiBlockToolbarPluginKey, view: (editorView) => { - this.view = new AIToolbarView(editorView, (state) => { + this.view = new AIBlockToolbarView(editorView, (state) => { this.emit("update", state); }); return this.view; @@ -159,7 +159,7 @@ export class AIToolbarProsemirrorPlugin extends EventEmitter { return this.view?.state?.show || false; } - public onUpdate(callback: (state: AIToolbarState) => void) { + public onUpdate(callback: (state: AIBlockToolbarState) => void) { return this.on("update", callback); } diff --git a/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts b/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts new file mode 100644 index 000000000..a0e737b87 --- /dev/null +++ b/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts @@ -0,0 +1,229 @@ +import { isNodeSelection, posToDOMRect } from "@tiptap/core"; +import { Plugin, PluginKey, PluginView } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; + +import { Block } from "../../blocks/defaultBlocks"; +import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; +import { EventEmitter } from "../../util/EventEmitter"; + +export type AIInlineToolbarState = UiElementPosition & { + prompt: string; + originalContent: Block[]; +}; + +export class AIInlineToolbarView implements PluginView { + public state?: AIInlineToolbarState; + public emitUpdate: () => void; + + constructor( + private readonly pmView: EditorView, + emitUpdate: (state: AIInlineToolbarState) => void + ) { + this.emitUpdate = () => { + if (!this.state) { + throw new Error("Attempting to update uninitialized AI toolbar"); + } + + emitUpdate(this.state); + }; + + pmView.dom.addEventListener("dragstart", this.dragHandler); + pmView.dom.addEventListener("dragover", this.dragHandler); + pmView.dom.addEventListener("blur", this.blurHandler); + pmView.dom.addEventListener("mousedown", this.closeHandler, true); + pmView.dom.addEventListener("keydown", this.closeHandler, true); + + // Setting capture=true ensures that any parent container of the editor that + // gets scrolled will trigger the scroll event. Scroll events do not bubble + // and so won't propagate to the document by default. + pmView.root.addEventListener("scroll", this.scrollHandler, true); + } + + blurHandler = (event: FocusEvent) => { + const editorWrapper = this.pmView.dom.parentElement!; + + // Checks if the focus is moving to an element outside the editor. If it is, + // the toolbar is hidden. + if ( + // An element is clicked. + event && + event.relatedTarget && + // Element is inside the editor. + (editorWrapper === (event.relatedTarget as Node) || + editorWrapper.contains(event.relatedTarget as Node) || + (event.relatedTarget as HTMLElement).matches( + ".bn-ui-container, .bn-ui-container *" + )) + ) { + return; + } + + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + }; + + // For dragging the whole editor. + dragHandler = () => { + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + }; + + closeHandler = () => this.close(); + + scrollHandler = () => { + if (this.state?.show) { + this.state.referencePos = this.getSelectionBoundingBox(); + this.emitUpdate(); + } + }; + + update(view: EditorView) { + const pluginState: AIInlineToolbarPluginState = + aiInlineToolbarPluginKey.getState(view.state); + + if (this.state && !this.state.show && !pluginState.open) { + return; + } + + if (pluginState.open) { + this.state = { + show: true, + referencePos: this.getSelectionBoundingBox(), + prompt: pluginState.prompt, + originalContent: pluginState.originalContent, + }; + + this.emitUpdate(); + + return; + } + + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + } + + destroy() { + this.pmView.dom.removeEventListener("dragstart", this.dragHandler); + this.pmView.dom.removeEventListener("dragover", this.dragHandler); + this.pmView.dom.removeEventListener("blur", this.blurHandler); + this.pmView.dom.removeEventListener("mousedown", this.closeHandler); + this.pmView.dom.removeEventListener("keydown", this.closeHandler); + + this.pmView.root.removeEventListener("scroll", this.scrollHandler, true); + } + + open(prompt: string, originalContent: Block[]) { + this.pmView.focus(); + this.pmView.dispatch( + this.pmView.state.tr.scrollIntoView().setMeta(aiInlineToolbarPluginKey, { + open: true, + prompt, + originalContent, + }) + ); + } + + close() { + this.pmView.focus(); + this.pmView.dispatch( + this.pmView.state.tr.scrollIntoView().setMeta(aiInlineToolbarPluginKey, { + open: false, + }) + ); + } + + closeMenu = () => { + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(); + } + }; + + getSelectionBoundingBox() { + const { state } = this.pmView; + const { selection } = state; + + // support for CellSelections + const { ranges } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const to = Math.max(...ranges.map((range) => range.$to.pos)); + + if (isNodeSelection(selection)) { + const node = this.pmView.nodeDOM(from) as HTMLElement; + + if (node) { + return node.getBoundingClientRect(); + } + } + + return posToDOMRect(this.pmView, from, to); + } +} + +type AIInlineToolbarPluginState = + | { open: true; prompt: string; originalContent: Block[] } + | { open: false }; + +export const aiInlineToolbarPluginKey = new PluginKey("AIInlineToolbarPlugin"); + +export class AIInlineToolbarProsemirrorPlugin extends EventEmitter { + private view: AIInlineToolbarView | undefined; + public readonly plugin: Plugin; + constructor() { + super(); + + this.plugin = new Plugin({ + key: aiInlineToolbarPluginKey, + + view: (editorView) => { + this.view = new AIInlineToolbarView(editorView, (state) => { + this.emit("update", state); + }); + return this.view; + }, + + state: { + // Initialize the plugin's internal state. + init(): AIInlineToolbarPluginState { + return { open: false }; + }, + + // Apply changes to the plugin state from an editor transaction. + apply(transaction, prev) { + const meta: AIInlineToolbarPluginState | undefined = + transaction.getMeta(aiInlineToolbarPluginKey); + + if (meta === undefined) { + return prev; + } + + return meta; + }, + }, + }); + } + + public open(prompt: string, originalContent: Block[]) { + this.view?.open(prompt, originalContent); + } + + public close() { + this.view?.close(); + } + + public get shown() { + return this.view?.state?.show || false; + } + + public onUpdate(callback: (state: AIInlineToolbarState) => void) { + return this.on("update", callback); + } + + public closeMenu = () => this.view!.closeMenu(); +} diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 4e0123b90..2347a14bb 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -278,11 +278,17 @@ export function getDefaultSlashMenuItems< type: "ai", }); }, - key: "ai", - ...editor.dictionary.slash_menu.ai, + key: "ai_block", + ...editor.dictionary.slash_menu.ai_block, }); } + items.push({ + onItemClick: () => editor.openSelectionMenu("`"), + key: "ai", + ...editor.dictionary.slash_menu.ai, + }); + items.push({ onItemClick: () => editor.openSelectionMenu(":"), key: "emoji", @@ -299,8 +305,9 @@ export function filterSuggestionItems< ({ title, aliases }) => title.toLowerCase().includes(query.toLowerCase()) || (aliases && - aliases.filter((alias) => - alias.toLowerCase().includes(query.toLowerCase()) + aliases.filter( + (alias) => + alias === "" || alias.toLowerCase().includes(query.toLowerCase()) ).length !== 0) ); } diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 543bc62e5..963a2f4ab 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -96,7 +96,7 @@ export const ar: Dictionary = { aliases: ["رمز تعبيري", "إيموجي", "إيموت", "عاطفة", "وجه"], group: "آخرون", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -256,6 +256,10 @@ export const ar: Dictionary = { align_justify: { tooltip: "ضبط النص", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -295,11 +299,17 @@ export const ar: Dictionary = { url_placeholder: "تحرير الرابط", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index 64f0ebf4a..cb26d5ae8 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -104,17 +104,34 @@ export const en = { aliases: ["file", "upload", "embed", "media", "url"], group: "Media", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Emoji", subtext: "Search for and insert an emoji", aliases: ["emoji", "emote", "emotion", "face"], group: "Others", }, - ai: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { @@ -270,6 +287,10 @@ export const en = { align_justify: { tooltip: "Justify text", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -309,11 +330,17 @@ export const en = { url_placeholder: "Edit URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index 581f09c30..bcd9aeaa4 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -111,7 +111,7 @@ export const fr: Dictionary = { aliases: ["emoji", "émoticône", "émotion", "visage"], group: "Autres", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -271,6 +271,10 @@ export const fr: Dictionary = { align_justify: { tooltip: "Justifier le texte", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -310,11 +314,17 @@ export const fr: Dictionary = { url_placeholder: "Modifier l'URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index 739e1e014..e1688b4cb 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -104,7 +104,7 @@ export const is: Dictionary = { aliases: ["emoji", "andlitsávísun", "tilfinningar", "andlit"], group: "Annað", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -263,6 +263,10 @@ export const is: Dictionary = { align_justify: { tooltip: "Jafna texta", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -302,11 +306,17 @@ export const is: Dictionary = { url_placeholder: "Breyta URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index d5999a541..4247983cc 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -131,7 +131,7 @@ export const ja: Dictionary = { aliases: ["絵文字", "顔文字", "感情表現", "顔"], group: "その他", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -291,6 +291,10 @@ export const ja: Dictionary = { align_justify: { tooltip: "両端揃え", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -330,11 +334,17 @@ export const ja: Dictionary = { url_placeholder: "URLを編集", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 4c31db60d..77d169b4e 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -124,7 +124,7 @@ export const ko: Dictionary = { ], group: "기타", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -284,6 +284,10 @@ export const ko: Dictionary = { align_justify: { tooltip: "텍스트 양쪽 맞춤", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -323,11 +327,17 @@ export const ko: Dictionary = { url_placeholder: "URL 수정", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index cd6ec5bb0..e7bdabadc 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -111,7 +111,7 @@ export const nl: Dictionary = { ], group: "Overig", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -270,6 +270,10 @@ export const nl: Dictionary = { align_justify: { tooltip: "Tekst uitvullen", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -309,11 +313,17 @@ export const nl: Dictionary = { url_placeholder: "Bewerk URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index bb3159fc6..ec9a9cf21 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -96,7 +96,7 @@ export const pl: Dictionary = { aliases: ["emoji", "emotka", "wyrażenie emocji", "twarz"], group: "Inne", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -255,6 +255,10 @@ export const pl: Dictionary = { align_justify: { tooltip: "Wyjustuj tekst", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -294,11 +298,17 @@ export const pl: Dictionary = { url_placeholder: "Edytuj URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 2877c64c9..292a91f42 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -103,7 +103,7 @@ export const pt: Dictionary = { aliases: ["emoji", "emoticon", "expressão emocional", "rosto"], group: "Outros", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -263,6 +263,10 @@ export const pt: Dictionary = { align_justify: { tooltip: "Justificar texto", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -302,11 +306,17 @@ export const pt: Dictionary = { url_placeholder: "Editar URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index 0b2869d1c..0fee25b97 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -138,7 +138,7 @@ export const ru: Dictionary = { aliases: ["эмодзи", "смайлик", "выражение эмоций", "лицо"], group: "Прочее", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -298,6 +298,10 @@ export const ru: Dictionary = { align_justify: { tooltip: "По середине текст", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -337,11 +341,17 @@ export const ru: Dictionary = { url_placeholder: "Изменить URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index e075bf6da..6d732e0d3 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -110,7 +110,7 @@ export const vi: Dictionary = { ], group: "Khác", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -270,6 +270,10 @@ export const vi: Dictionary = { align_justify: { tooltip: "Căn đều văn bản", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -309,11 +313,17 @@ export const vi: Dictionary = { url_placeholder: "Chỉnh sửa URL", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index dccc5faf3..a23dbefe2 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -144,7 +144,7 @@ export const zh: Dictionary = { ], group: "其他", }, - ai: { + ai_block: { title: "AI Block", subtext: "Create content using generative AI", aliases: ["ai", "artificial intelligence", "generate"], @@ -304,6 +304,10 @@ export const zh: Dictionary = { align_justify: { tooltip: "文本对齐", }, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, }, file_panel: { upload: { @@ -343,11 +347,17 @@ export const zh: Dictionary = { url_placeholder: "编辑链接地址", }, }, - ai_toolbar: { + ai_block_toolbar: { show_prompt: "Show prompt", update: "Update", updating: "Updating…", }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5d626244c..008a60b08 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,6 +3,7 @@ export * from "./api/exporters/html/externalHTMLExporter"; export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testUtil"; export * from "./blocks/AIBlockContent/AIBlockContent"; +export * from "./blocks/AIBlockContent/mockAIOperation"; export * from "./blocks/AudioBlockContent/AudioBlockContent"; export * from "./blocks/FileBlockContent/FileBlockContent"; export * from "./blocks/ImageBlockContent/ImageBlockContent"; @@ -46,3 +47,4 @@ export * from "./extensions/UniqueID/UniqueID"; export * from "./api/exporters/markdown/markdownExporter"; export * from "./api/parsers/html/parseHTML"; export * from "./api/parsers/markdown/parseMarkdown"; +export { mockAIOperation } from "./blocks/AIBlockContent/mockAIOperation"; diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index ad9ab55d3..3c0c2c14a 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -55,7 +55,11 @@ export * from "./BlockNoteTheme"; export * from "./defaultThemes"; export const components: Components = { - AIToolbar: { + AIBlockToolbar: { + Root: Toolbar, + Button: ToolbarButton, + }, + AIInlineToolbar: { Root: Toolbar, Button: ToolbarButton, }, diff --git a/packages/react/package.json b/packages/react/package.json index cd50b5071..46f6f3bae 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -54,6 +54,7 @@ "@blocknote/core": "^0.15.3", "@floating-ui/react": "^0.26.4", "@tiptap/core": "^2.5.0", + "@tiptap/pm": "2.5.0", "@tiptap/react": "^2.5.0", "lodash.merge": "^4.6.2", "react": "^18", diff --git a/packages/react/src/components/AIToolbar/AIToolbar.tsx b/packages/react/src/components/AIBlockToolbar/AIBlockToolbar.tsx similarity index 69% rename from packages/react/src/components/AIToolbar/AIToolbar.tsx rename to packages/react/src/components/AIBlockToolbar/AIBlockToolbar.tsx index b4b348932..33dd7bc09 100644 --- a/packages/react/src/components/AIToolbar/AIToolbar.tsx +++ b/packages/react/src/components/AIBlockToolbar/AIBlockToolbar.tsx @@ -1,12 +1,12 @@ import { ReactNode, useState } from "react"; import { useComponentsContext } from "../../editor/ComponentsContext"; -import { AIToolbarProps } from "./AIToolbarProps"; +import { AIBlockToolbarProps } from "./AIBlockToolbarProps"; import { ShowPromptButton } from "./DefaultButtons/ShowPromptButton"; import { UpdateButton } from "./DefaultButtons/UpdateButton"; -export const getAIToolbarItems = ( - props: AIToolbarProps & { +export const getAIBlockToolbarItems = ( + props: AIBlockToolbarProps & { updating: boolean; setUpdating: (updating: boolean) => void; } @@ -24,14 +24,17 @@ export const getAIToolbarItems = ( * - Custom buttons: Buttons made using the Components.AIToolbar.Button * component from the component context. */ -export const AIToolbar = (props: AIToolbarProps & { children?: ReactNode }) => { +export const AIBlockToolbar = ( + props: AIBlockToolbarProps & { children?: ReactNode } +) => { const Components = useComponentsContext()!; const [updating, setUpdating] = useState(false); return ( - - {props.children || getAIToolbarItems({ ...props, updating, setUpdating })} - + + {props.children || + getAIBlockToolbarItems({ ...props, updating, setUpdating })} + ); }; diff --git a/packages/react/src/components/AIToolbar/AIToolbarController.tsx b/packages/react/src/components/AIBlockToolbar/AIBlockToolbarController.tsx similarity index 77% rename from packages/react/src/components/AIToolbar/AIToolbarController.tsx rename to packages/react/src/components/AIBlockToolbar/AIBlockToolbarController.tsx index f6e665cdf..83b84c648 100644 --- a/packages/react/src/components/AIToolbar/AIToolbarController.tsx +++ b/packages/react/src/components/AIBlockToolbar/AIBlockToolbarController.tsx @@ -5,11 +5,11 @@ import { FC } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; import { useUIPluginState } from "../../hooks/useUIPluginState"; -import { AIToolbar } from "./AIToolbar"; -import { AIToolbarProps } from "./AIToolbarProps"; +import { AIBlockToolbar } from "./AIBlockToolbar"; +import { AIBlockToolbarProps } from "./AIBlockToolbarProps"; -export const AIToolbarController = (props: { - aiToolbar?: FC; +export const AIBlockToolbarController = (props: { + aiToolbar?: FC; }) => { const editor = useBlockNoteEditor< BlockSchema, @@ -17,14 +17,14 @@ export const AIToolbarController = (props: { StyleSchema >(); - if (!editor.aiToolbar) { + if (!editor.aiBlockToolbar) { throw new Error( "AIToolbarController can only be used when BlockNote editor schema contains an AI block" ); } const state = useUIPluginState( - editor.aiToolbar.onUpdate.bind(editor.aiToolbar) + editor.aiBlockToolbar.onUpdate.bind(editor.aiBlockToolbar) ); const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning( @@ -49,7 +49,7 @@ export const AIToolbarController = (props: { const { prompt } = state; - const Component = props.aiToolbar || AIToolbar; + const Component = props.aiToolbar || AIBlockToolbar; return (
diff --git a/packages/react/src/components/AIBlockToolbar/AIBlockToolbarProps.ts b/packages/react/src/components/AIBlockToolbar/AIBlockToolbarProps.ts new file mode 100644 index 000000000..3dc8b5108 --- /dev/null +++ b/packages/react/src/components/AIBlockToolbar/AIBlockToolbarProps.ts @@ -0,0 +1,3 @@ +export type AIBlockToolbarProps = { + prompt: string; +}; diff --git a/packages/react/src/components/AIToolbar/DefaultButtons/ShowPromptButton.tsx b/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx similarity index 81% rename from packages/react/src/components/AIToolbar/DefaultButtons/ShowPromptButton.tsx rename to packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx index 9e1b86c66..9bdd62fe6 100644 --- a/packages/react/src/components/AIToolbar/DefaultButtons/ShowPromptButton.tsx +++ b/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx @@ -2,7 +2,7 @@ import { aiBlockConfig, BlockSchemaWithBlock, InlineContentSchema, - mockAIModelCall, + mockAIOperation, StyleSchema, } from "@blocknote/core"; import { @@ -17,10 +17,10 @@ import { RiSparkling2Fill } from "react-icons/ri"; import { useComponentsContext } from "../../../editor/ComponentsContext"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; import { useDictionary } from "../../../i18n/dictionary"; -import { AIToolbarProps } from "../AIToolbarProps"; +import { AIBlockToolbarProps } from "../AIBlockToolbarProps"; export const ShowPromptButton = ( - props: AIToolbarProps & { + props: AIBlockToolbarProps & { setUpdating: (updating: boolean) => void; } ) => { @@ -47,13 +47,18 @@ export const ShowPromptButton = ( props.setUpdating(true); setOpened(false); editor.updateBlock(editor.getTextCursorPosition().block, { - props: { prompt: props.prompt }, - content: await mockAIModelCall(props.prompt), + props: { prompt: currentEditingPrompt }, + content: ( + await mockAIOperation(editor, props.prompt, { + operation: "replaceBlock", + blockIdentifier: editor.getTextCursorPosition().block, + }) + )[0].content as any, }); props.setUpdating(false); } }, - [editor, props] + [currentEditingPrompt, editor, props] ); const handleChange = useCallback( @@ -72,14 +77,18 @@ export const ShowPromptButton = ( }; }, [editor.domElement]); + if (!editor.isEditable) { + return null; + } + return ( - {dict.ai_toolbar.show_prompt} + {dict.ai_block_toolbar.show_prompt} void; } @@ -31,19 +31,26 @@ export const UpdateButton = ( } return ( - { props.setUpdating(true); editor.focus(); editor.updateBlock(editor.getTextCursorPosition().block, { props: { prompt: props.prompt }, - content: await mockAIModelCall(props.prompt), + content: ( + await mockAIOperation(editor, props.prompt, { + operation: "replaceBlock", + blockIdentifier: editor.getTextCursorPosition().block, + }) + )[0].content as any, }); props.setUpdating(false); }}> - {props.updating ? dict.ai_toolbar.updating : dict.ai_toolbar.update} - + {props.updating + ? dict.ai_block_toolbar.updating + : dict.ai_block_toolbar.update} + ); }; diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx b/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx new file mode 100644 index 000000000..0ff8335f7 --- /dev/null +++ b/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx @@ -0,0 +1,42 @@ +import { ReactNode, useState } from "react"; + +import { useComponentsContext } from "../../editor/ComponentsContext"; +import { AIInlineToolbarProps } from "./AIInlineToolbarProps"; +import { AcceptButton } from "./DefaultButtons/AcceptButton"; +import { RetryButton } from "./DefaultButtons/RetryButton"; +import { RevertButton } from "./DefaultButtons/RevertButton"; + +export const getAIBlockToolbarItems = ( + props: AIInlineToolbarProps & { + updating: boolean; + setUpdating: (updating: boolean) => void; + } +): JSX.Element[] => [ + , + , + , +]; + +/** + * By default, the AIToolbar component will render with default buttons. + * However, you can override the selects/buttons to render by passing children. + * The children you pass should be: + * + * - Default buttons: Components found within the `/DefaultButtons` directory. + * - Custom buttons: Buttons made using the Components.AIToolbar.Button + * component from the component context. + */ +export const AIInlineToolbar = ( + props: AIInlineToolbarProps & { children?: ReactNode } +) => { + const Components = useComponentsContext()!; + + const [updating, setUpdating] = useState(true); + + return ( + + {props.children || + getAIBlockToolbarItems({ ...props, updating, setUpdating })} + + ); +}; diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx new file mode 100644 index 000000000..6f026f4ea --- /dev/null +++ b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx @@ -0,0 +1,53 @@ +import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { flip, offset } from "@floating-ui/react"; +import { FC } from "react"; + +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; +import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; +import { useUIPluginState } from "../../hooks/useUIPluginState"; +import { AIInlineToolbar } from "./AIInlineToolbar"; +import { AIInlineToolbarProps } from "./AIInlineToolbarProps"; + +export const AIInlineToolbarController = (props: { + aiToolbar?: FC; +}) => { + const editor = useBlockNoteEditor< + BlockSchema, + InlineContentSchema, + StyleSchema + >(); + + const state = useUIPluginState( + editor.aiInlineToolbar.onUpdate.bind(editor.aiInlineToolbar) + ); + + const { isMounted, ref, style, getFloatingProps } = useUIElementPositioning( + state?.show || false, + state?.referencePos || null, + 3000, + { + placement: "top-start", + middleware: [offset(10), flip()], + onOpenChange: (open) => { + if (!open) { + editor.linkToolbar.closeMenu(); + editor.focus(); + } + }, + } + ); + + if (!isMounted || !state) { + return null; + } + + const { prompt, originalContent } = state; + + const Component = props.aiToolbar || AIInlineToolbar; + + return ( +
+ +
+ ); +}; diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts new file mode 100644 index 000000000..8ed42f73c --- /dev/null +++ b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts @@ -0,0 +1,6 @@ +import { Block } from "@blocknote/core"; + +export type AIInlineToolbarProps = { + prompt: string; + originalContent: Block[]; +}; diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx b/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx new file mode 100644 index 000000000..28dec4114 --- /dev/null +++ b/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx @@ -0,0 +1,38 @@ +import { + aiBlockConfig, + BlockSchemaWithBlock, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; + +import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../i18n/dictionary"; +import { RiCheckFill } from "react-icons/ri"; + +export const AcceptButton = () => { + const dict = useDictionary(); + const Components = useComponentsContext()!; + + const editor = useBlockNoteEditor< + BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, + InlineContentSchema, + StyleSchema + >(); + + if (!editor.isEditable) { + return null; + } + + return ( + } + mainTooltip={dict.ai_inline_toolbar.accept} + label={dict.ai_inline_toolbar.accept} + onClick={async () => { + editor.aiInlineToolbar.close(); + }} + /> + ); +}; diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx new file mode 100644 index 000000000..50b8dd340 --- /dev/null +++ b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx @@ -0,0 +1,80 @@ +import { + aiBlockConfig, + BlockSchemaWithBlock, + InlineContentSchema, + mockAIOperation, + StyleSchema, +} from "@blocknote/core"; +import { TextSelection } from "prosemirror-state"; +import { RiLoopLeftFill } from "react-icons/ri"; + +import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../i18n/dictionary"; +import { AIInlineToolbarProps } from "../AIInlineToolbarProps"; + +export const RetryButton = ( + props: AIInlineToolbarProps & { + updating: boolean; + setUpdating: (updating: boolean) => void; + } +) => { + const dict = useDictionary(); + const Components = useComponentsContext()!; + + const editor = useBlockNoteEditor< + BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, + InlineContentSchema, + StyleSchema + >(); + + if (!editor.isEditable) { + return null; + } + + return ( + } + mainTooltip={dict.ai_inline_toolbar.retry} + label={dict.ai_inline_toolbar.retry} + onClick={async () => { + editor.focus(); + + props.setUpdating(true); + + const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; + const startPos = editor._tiptapEditor.state.selection.from; + const endPos = editor._tiptapEditor.state.selection.to; + const currentContent = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + + editor.replaceBlocks( + currentContent, + (await mockAIOperation(editor, props.prompt, { + operation: "replaceSelection", + })) as any[] + ); + + const newDocSize = editor._tiptapEditor.state.doc.nodeSize; + + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.state.tr.setSelection( + TextSelection.create( + editor._tiptapEditor.state.doc, + startPos, + endPos + newDocSize - oldDocSize + ) + ) + ); + + props.setUpdating(false); + editor.formattingToolbar.closeMenu(); + editor.focus(); + editor.aiInlineToolbar.open(props.prompt, props.originalContent); + }}> + {props.updating && dict.ai_inline_toolbar.updating} + + ); +}; diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx new file mode 100644 index 000000000..fc83a853f --- /dev/null +++ b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx @@ -0,0 +1,64 @@ +import { + aiBlockConfig, + BlockSchemaWithBlock, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { TextSelection } from "prosemirror-state"; +import { RiArrowGoBackFill } from "react-icons/ri"; + +import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../i18n/dictionary"; +import { AIInlineToolbarProps } from "../AIInlineToolbarProps"; + +export const RevertButton = (props: AIInlineToolbarProps) => { + const dict = useDictionary(); + const Components = useComponentsContext()!; + + const editor = useBlockNoteEditor< + BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, + InlineContentSchema, + StyleSchema + >(); + + if (!editor.isEditable) { + return null; + } + + return ( + } + mainTooltip={dict.ai_inline_toolbar.revert} + label={dict.ai_inline_toolbar.revert} + onClick={() => { + editor.focus(); + const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; + const startPos = editor._tiptapEditor.state.selection.from; + const endPos = editor._tiptapEditor.state.selection.to; + const replacedContent = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + + editor.replaceBlocks(replacedContent, props.originalContent as any[]); + + const newDocSize = editor._tiptapEditor.state.doc.nodeSize; + + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.state.tr.setSelection( + TextSelection.create( + editor._tiptapEditor.state.doc, + startPos, + endPos + newDocSize - oldDocSize + ) + ) + ); + + editor.formattingToolbar.closeMenu(); + editor.focus(); + editor.aiInlineToolbar.close(); + }} + /> + ); +}; diff --git a/packages/react/src/components/AIToolbar/AIToolbarProps.ts b/packages/react/src/components/AIToolbar/AIToolbarProps.ts deleted file mode 100644 index cfcc7086c..000000000 --- a/packages/react/src/components/AIToolbar/AIToolbarProps.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AIToolbarProps = { - prompt: string; -}; diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx new file mode 100644 index 000000000..710aef58e --- /dev/null +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx @@ -0,0 +1,116 @@ +import { + BlockSchema, + InlineContentSchema, + mockAIOperation, + StyleSchema, +} from "@blocknote/core"; +import { TextSelection } from "@tiptap/pm/state"; +import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react"; +import { RiSparkling2Fill } from "react-icons/ri"; + +import { useDictionary } from "../../../i18n/dictionary"; +import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; + +export const AIButton = () => { + const dict = useDictionary(); + const Components = useComponentsContext()!; + + const editor = useBlockNoteEditor< + BlockSchema, + InlineContentSchema, + StyleSchema + >(); + + const [currentEditingPrompt, setCurrentEditingPrompt] = useState(""); + const [updating, setUpdating] = useState(false); + + const runAIEdit = useCallback( + async (prompt: string) => { + setUpdating(true); + + const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; + const startPos = editor._tiptapEditor.state.selection.from; + const endPos = editor._tiptapEditor.state.selection.to; + const originalContent = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + + editor.replaceBlocks( + originalContent, + (await mockAIOperation(editor, prompt, { + operation: "replaceSelection", + })) as any[] + ); + + const newDocSize = editor._tiptapEditor.state.doc.nodeSize; + + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.state.tr.setSelection( + TextSelection.create( + editor._tiptapEditor.state.doc, + startPos, + endPos + newDocSize - oldDocSize + ) + ) + ); + + setUpdating(false); + editor.formattingToolbar.closeMenu(); + editor.focus(); + editor.aiInlineToolbar.open(prompt, originalContent); + }, + [editor] + ); + + const handleEnter = useCallback( + async (event: KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); + await runAIEdit(currentEditingPrompt); + } + }, + [currentEditingPrompt, runAIEdit] + ); + + const handleChange = useCallback( + (event: ChangeEvent) => + setCurrentEditingPrompt(event.currentTarget.value), + [] + ); + + if (!editor.isEditable) { + return null; + } + + return ( + + + }> + {updating && "Updating..."} + + + + + } + value={currentEditingPrompt || ""} + autoFocus={true} + placeholder={dict.formatting_toolbar.ai.input_placeholder} + onKeyDown={handleEnter} + onChange={handleChange} + /> + + runAIEdit("Make longer")}> + Make longer + + + + ); +}; diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index 1f4b90d59..457eca2b1 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -95,7 +95,7 @@ export const blockTypeSelectItems = ( isSelected: (block) => block.type === "checkListItem", }, { - name: dict.slash_menu.ai.title, + name: dict.slash_menu.ai_block.title, type: "ai", icon: RiSparkling2Fill, isSelected: (block) => block.type === "ai", diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx index ba591a608..23bb3276b 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx @@ -20,6 +20,7 @@ import { FileRenameButton } from "./DefaultButtons/FileRenameButton"; import { FileDownloadButton } from "./DefaultButtons/FileDownloadButton"; import { FilePreviewButton } from "./DefaultButtons/FilePreviewButton"; import { FileDeleteButton } from "./DefaultButtons/FileDeleteButton"; +import { AIButton } from "./DefaultButtons/AIButton"; export const getFormattingToolbarItems = ( blockTypeSelectItems?: BlockTypeSelectItem[] @@ -45,6 +46,7 @@ export const getFormattingToolbarItems = ( , , , + , ]; // TODO: props.blockTypeSelectItems should only be available if no children diff --git a/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx new file mode 100644 index 000000000..52f5b5910 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx @@ -0,0 +1,61 @@ +import { + BlockNoteEditor, + BlockSchema, + InlineContentSchema, + mockAIOperation, + StyleSchema, +} from "@blocknote/core"; +import { DefaultReactSuggestionItem } from "./types"; +import { TextSelection } from "@tiptap/pm/state"; + +// TODO: Maybe we don't want to define the default AI prompts based on the +// dictionary +export function getDefaultAIMenuItems< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + query: string +): DefaultReactSuggestionItem[] { + return Object.values(editor.dictionary.ai_menu).map((item) => ({ + ...item, + onItemClick: async () => { + const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; + const startPos = editor._tiptapEditor.state.selection.from; + const endPos = editor._tiptapEditor.state.selection.to; + const originalContent = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + + const prompt = + item.title === editor.dictionary.ai_menu.custom_prompt.title + ? query + : item.title; + + editor.replaceBlocks( + originalContent, + (await mockAIOperation(editor, prompt, { + operation: "replaceBlock", + blockIdentifier: editor.getTextCursorPosition().block, + })) as any[] + ); + + const newDocSize = editor._tiptapEditor.state.doc.nodeSize; + + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.state.tr.setSelection( + TextSelection.create( + editor._tiptapEditor.state.doc, + startPos, + endPos + newDocSize - oldDocSize + ) + ) + ); + + editor.formattingToolbar.closeMenu(); + editor.focus(); + editor.aiInlineToolbar.open(prompt, originalContent); + }, + })); +} diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx index d991da10b..9dde41c4f 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx @@ -36,6 +36,7 @@ const icons = { video: RiFilmLine, audio: RiVolumeUpFill, file: RiFile2Line, + ai_block: RiSparkling2Fill, ai: RiSparkling2Fill, emoji: RiEmotionFill, }; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 8de98ef59..3f101ee5b 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -6,7 +6,10 @@ import { SuggestionMenuController } from "../components/SuggestionMenu/Suggestio import { GridSuggestionMenuController } from "../components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; -import { AIToolbarController } from "../components/AIToolbar/AIToolbarController"; +import { AIBlockToolbarController } from "../components/AIBlockToolbar/AIBlockToolbarController"; +import { AIInlineToolbarController } from "../components/AIInlineToolbar/AIInlineToolbarController"; +import { filterSuggestionItems } from "@blocknote/core"; +import { getDefaultAIMenuItems } from "../components/SuggestionMenu/getDefaultAIMenuItems"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -16,7 +19,9 @@ export type BlockNoteDefaultUIProps = { filePanel?: boolean; tableHandles?: boolean; emojiPicker?: boolean; - aiToolbar?: boolean; + aiBlockToolbar?: boolean; + aiInlineToolbar?: boolean; + aiMenu?: boolean; }; export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { @@ -47,7 +52,18 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { {editor.tableHandles && props.tableHandles !== false && ( )} - {editor.aiToolbar && props.aiToolbar !== false && } + {editor.aiBlockToolbar && props.aiBlockToolbar !== false && ( + + )} + {props.aiInlineToolbar !== false && } + {props.aiMenu !== false && ( + + filterSuggestionItems(getDefaultAIMenuItems(editor, query), query) + } + /> + )} ); } diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index 99744ae8d..7fe74833f 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -13,7 +13,25 @@ import { DefaultReactSuggestionItem } from "../components/SuggestionMenu/types"; import { DefaultReactGridSuggestionItem } from "../components/SuggestionMenu/GridSuggestionMenu/types"; export type ComponentProps = { - AIToolbar: { + AIBlockToolbar: { + Root: { + className?: string; + children?: ReactNode; + }; + Button: { + className?: string; + mainTooltip?: string; + secondaryTooltip?: string; + icon?: ReactNode; + onClick?: (e: MouseEvent) => void; + isSelected?: boolean; + isDisabled?: boolean; + } & ( + | { children: ReactNode; label?: string } + | { children?: undefined; label: string } + ); + }; + AIInlineToolbar: { Root: { className?: string; children?: ReactNode; diff --git a/packages/shadcn/src/index.tsx b/packages/shadcn/src/index.tsx index 506ef1f18..69ff0e7a9 100644 --- a/packages/shadcn/src/index.tsx +++ b/packages/shadcn/src/index.tsx @@ -49,7 +49,11 @@ import { Toolbar, ToolbarButton, ToolbarSelect } from "./toolbar/Toolbar"; import "./style.css"; export const components: Components = { - AIToolbar: { + AIBlockToolbar: { + Root: Toolbar, + Button: ToolbarButton, + }, + AIInlineToolbar: { Root: Toolbar, Button: ToolbarButton, }, From d48d91e4747b5408e326a87aadc6c808461ebc48 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 6 Aug 2024 22:06:02 +0200 Subject: [PATCH 03/45] Small fix --- .../react/src/components/AIInlineToolbar/AIInlineToolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx b/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx index 0ff8335f7..f6e70fc15 100644 --- a/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx +++ b/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx @@ -31,7 +31,7 @@ export const AIInlineToolbar = ( ) => { const Components = useComponentsContext()!; - const [updating, setUpdating] = useState(true); + const [updating, setUpdating] = useState(false); return ( From 82a56a9ff57d9c738ef20ea9d6a39c048c391038 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 7 Aug 2024 00:14:37 +0200 Subject: [PATCH 04/45] UX improvements & refactor --- .../blocks/AIBlockContent/AIBlockContent.ts | 20 +- .../blocks/AIBlockContent/mockAIFunctions.ts | 335 ++++++++++++++++++ .../blocks/AIBlockContent/mockAIOperation.ts | 263 -------------- .../AIInlineToolbar/AIInlineToolbarPlugin.ts | 22 +- packages/core/src/i18n/locales/ar.ts | 27 +- packages/core/src/i18n/locales/fr.ts | 27 +- packages/core/src/i18n/locales/is.ts | 27 +- packages/core/src/i18n/locales/ja.ts | 27 +- packages/core/src/i18n/locales/ko.ts | 27 +- packages/core/src/i18n/locales/nl.ts | 27 +- packages/core/src/i18n/locales/pl.ts | 27 +- packages/core/src/i18n/locales/pt.ts | 27 +- packages/core/src/i18n/locales/ru.ts | 27 +- packages/core/src/i18n/locales/vi.ts | 27 +- packages/core/src/i18n/locales/zh.ts | 27 +- packages/core/src/index.ts | 3 +- .../DefaultButtons/ShowPromptButton.tsx | 16 +- .../DefaultButtons/UpdateButton.tsx | 16 +- .../AIInlineToolbar/AIInlineToolbar.tsx | 47 ++- .../AIInlineToolbarController.tsx | 4 +- .../AIInlineToolbar/AIInlineToolbarProps.ts | 4 +- .../DefaultButtons/AcceptButton.tsx | 18 +- .../DefaultButtons/RetryButton.tsx | 49 +-- .../DefaultButtons/RevertButton.tsx | 30 +- .../DefaultButtons/AIButton.tsx | 48 +-- .../SuggestionMenu/getDefaultAIMenuItems.tsx | 39 +- 26 files changed, 682 insertions(+), 529 deletions(-) create mode 100644 packages/core/src/blocks/AIBlockContent/mockAIFunctions.ts delete mode 100644 packages/core/src/blocks/AIBlockContent/mockAIOperation.ts diff --git a/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts b/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts index 53f7adc38..afd4b3495 100644 --- a/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts +++ b/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts @@ -6,7 +6,7 @@ import { PropSchema, } from "../../schema"; import { defaultProps } from "../defaultProps"; -import { mockAIOperation } from "./mockAIOperation"; +import { mockAIReplaceBlockContent } from "./mockAIFunctions"; export const aiPropSchema = { ...defaultProps, @@ -26,20 +26,12 @@ export const aiRender = ( editor: BlockNoteEditor ) => { if (!block.props.prompt) { - const generateResponseCallback = async () => { + const replaceContent = () => { const prompt = span.innerText; + // TODO: Updating text content in this way isn't working generateButton.textContent = "Generating..."; - const response = await mockAIOperation(editor, prompt, { - operation: "replaceBlock", - blockIdentifier: block.id, - }); - - editor.updateBlock(block, { - type: "ai", - props: { prompt }, - content: response[0].content, - }); + mockAIReplaceBlockContent(editor, prompt, block.id); }; const promptBox = document.createElement("div"); @@ -66,7 +58,7 @@ export const aiRender = ( event.preventDefault(); event.stopPropagation(); - generateResponseCallback(); + replaceContent(); } }, true @@ -76,7 +68,7 @@ export const aiRender = ( const generateButton = document.createElement("button"); generateButton.contentEditable = "false"; generateButton.textContent = "Generate"; - generateButton.addEventListener("click", generateResponseCallback); + generateButton.addEventListener("click", replaceContent); promptBox.appendChild(generateButton); return { diff --git a/packages/core/src/blocks/AIBlockContent/mockAIFunctions.ts b/packages/core/src/blocks/AIBlockContent/mockAIFunctions.ts new file mode 100644 index 000000000..bf74b4272 --- /dev/null +++ b/packages/core/src/blocks/AIBlockContent/mockAIFunctions.ts @@ -0,0 +1,335 @@ +import { TextSelection } from "@tiptap/pm/state"; + +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import type { BlockIdentifier } from "../../schema"; +import type { Block } from "../defaultBlocks"; + +const flattenBlocks = ( + blocks: Block[] +): Block[] => { + // Un-nests blocks, as we're using Markdown to convert the blocks to a prompt + // for the AI model anyway. + let noNestedBlocks = false; + while (!noNestedBlocks) { + noNestedBlocks = true; + + for (let i = blocks.length - 1; i >= 0; i--) { + const children = blocks[i].children; + + if (children.length !== 0) { + noNestedBlocks = false; + blocks[i].children = []; + blocks.splice(i + 1, 0, ...children); + } + } + } + + return blocks; +}; + +// Mock function to edit the blocks using AI. Has 3 operation modes: +// - replaceSelection: Replaces the selected text with AI generated text. To +// make things easier, this actually replaces the selected blocks, and the final +// prompt specifies to only modify the selected text. +// - replaceBlock: Replaces a block with AI generated content. The block has to +// be specified when calling the function. +// - insert: Inserts AI generated text at the text cursor position. Assumes that +// a selection isn't active. To make things easier, this actually replaces the +// block containing the text cursor, and the final prompt specifies to only +// append content to it. +export const mockAICall = async (_prompt: string) => + new Promise((resolve) => { + setTimeout(() => { + resolve( + 'Over time, a variety of different cat breeds have emerged. One of these is the Maine Coon, which is one of the largest domesticated cat breeds. Known for their friendly and sociable nature, Maine Coons are often referred to as "gentle giants." They have a distinctive physical appearance, characterized by their tufted ears, bushy tails, and long, shaggy fur that is well-suited for cold climates. Maine Coons are also intelligent and playful, making them excellent companions for families.\n' + ); + }, 1000); + }); + +// Replaces the selected text with AI generated text. To make things easier, +// this actually replaces the selected blocks, and the final prompt specifies +// to only modify the selected text. +export const mockAIReplaceSelection = async ( + editor: BlockNoteEditor, + prompt: string +): Promise[]> => { + const blocks = flattenBlocks(editor.document); + + // Gets selection and blocks size, so we know where to set the selection + // after the content is replaced. + const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; + const startPos = editor._tiptapEditor.state.selection.from; + const endPos = editor._tiptapEditor.state.selection.to; + + const selectedBlocks = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + const firstSelectedBlockIndex = blocks.findIndex( + (block) => block.id === selectedBlocks[0].id + ); + const lastSelectedBlockIndex = blocks.findIndex( + (block) => block.id === selectedBlocks[selectedBlocks.length - 1].id + ); + + // We split the blocks to provide the AI model with full context of what + // exactly is selected. + const before = blocks.slice(0, firstSelectedBlockIndex); + const selected = blocks.slice( + firstSelectedBlockIndex, + lastSelectedBlockIndex + 1 + ); + const after = blocks.slice(lastSelectedBlockIndex + 1); + const beforeMarkdown = await editor.blocksToMarkdownLossy(before); + const selectedMarkdown = await editor.blocksToMarkdownLossy(selected); + const afterMarkdown = await editor.blocksToMarkdownLossy(after); + + // We also split the text in the selected section to provide more exact + // context for the selection. + const beforeText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.$from.start(), + editor._tiptapEditor.state.selection.from + ); + const selectedText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.from, + editor._tiptapEditor.state.selection.to + ); + const afterText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.to, + editor._tiptapEditor.state.selection.$to.end() + ); + + const fullPrompt = `Below is the content of an editable Markdown blocks. It's split into sections to show where the user selection is located. + +Content before: +\`\`\` +${beforeMarkdown} +\`\`\` +The Markdown content in the blocks before the selected section. + +Selected section: +\`\`\` +${selectedMarkdown} +\`\`\` +The Markdown content of the selected section. + +Content after: +\`\`\` +${afterMarkdown} +\`\`\` +The Markdown content in the blocks after the selected section. + +Text before: +\`\`\` +${beforeText} +\`\`\` +The text between the start of the selected section and the start of the user selection. + +Selected text: +\`\`\` +${selectedText} +\`\`\` +The user selected text within the selected section. + +Text after: +\`\`\` +${afterText} +\`\`\` +The text between the end of the user selection and the end of the selected section. + +Prompt: +\`\`\` +${prompt} +\`\`\` +The AI prompt provided by the user. + +Provide a version of the selected section where the selected text inside is modified based on the prompt. Provide Markdown only, the response should not contain any additional text. +`; + + const aiResponse = await mockAICall(fullPrompt); + const replacementBlocks = await editor.tryParseMarkdownToBlocks(aiResponse); + editor.replaceBlocks(selectedBlocks, replacementBlocks); + + const newDocSize = editor._tiptapEditor.state.doc.nodeSize; + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.state.tr.setSelection( + TextSelection.create( + editor._tiptapEditor.state.doc, + startPos, + endPos + newDocSize - oldDocSize + ) + ) + ); + + editor.formattingToolbar.closeMenu(); + editor.focus(); + editor.aiInlineToolbar.open(prompt, "replaceSelection"); + + return selectedBlocks; +}; + +export const mockAIReplaceBlockContent = async ( + editor: BlockNoteEditor, + prompt: string, + blockIdentifier: BlockIdentifier +): Promise[]> => { + const blocks = flattenBlocks(editor.document); + + const blockID = + typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; + + const selectedBlock = editor.getBlock(blockIdentifier)!; + const selectedBlockIndex = blocks.findIndex((block) => block.id === blockID); + + // We split the blocks to provide the AI model with full context of what + // exactly is selected. + const before = blocks.slice(0, selectedBlockIndex); + const selected = blocks[selectedBlockIndex]; + const after = blocks.slice(selectedBlockIndex + 1); + const beforeMarkdown = await editor.blocksToMarkdownLossy(before); + const selectedMarkdown = await editor.blocksToMarkdownLossy([selected]); + const afterMarkdown = await editor.blocksToMarkdownLossy(after); + + const fullPrompt = `Below is the content of an editable Markdown blocks. It's split into sections to show where the user selection is located. + +Content before: +\`\`\` +${beforeMarkdown} +\`\`\` +The Markdown content in the blocks before the selected section. + +Selected section: +\`\`\` +${selectedMarkdown} +\`\`\` +The Markdown content of the selected section. + +Content after: +\`\`\` +${afterMarkdown} +\`\`\` +The Markdown content in the blocks after the selected section. + +Prompt: +\`\`\` +${prompt} +\`\`\` +The AI prompt provided by the user. + +Replace the selected section with content based on the prompt. Provide Markdown only, the response should not contain any additional text. +`; + + const aiResponse = await mockAICall(fullPrompt); + const replacementBlocks = await editor.tryParseMarkdownToBlocks(aiResponse); + editor.updateBlock(editor.getTextCursorPosition().block, { + props: { prompt }, + content: replacementBlocks[0].content, + }); + + // TODO: Selection update + + editor.focus(); + + return [selectedBlock]; +}; + +export const mockAIInsertAfterSelection = async ( + editor: BlockNoteEditor, + prompt: string +): Promise[]> => { + const blocks = flattenBlocks(editor.document); + + // Gets selection and blocks size, so we know where to set the selection + // after the content is replaced. + const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; + const endPos = editor._tiptapEditor.state.selection.to; + + const selection = editor.getSelection(); + const selectedBlock = + selection?.blocks[selection?.blocks.length - 1] || + editor.getTextCursorPosition().block; + const selectedBlockIndex = blocks.findIndex( + (block) => block.id === selectedBlock.id + ); + + // We split the blocks to provide the AI model with full context of what + // exactly is selected. + const before = blocks.slice(0, selectedBlockIndex); + const selected = blocks[selectedBlockIndex]; + const after = blocks.slice(selectedBlockIndex + 1); + const beforeMarkdown = await editor.blocksToMarkdownLossy(before); + const selectedMarkdown = await editor.blocksToMarkdownLossy([selected]); + const afterMarkdown = await editor.blocksToMarkdownLossy(after); + + // We also split the text in the selected section to provide more exact + // context for the selection. + const beforeText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.$from.start(), + editor._tiptapEditor.state.selection.to + ); + const afterText = editor._tiptapEditor.state.doc.textBetween( + editor._tiptapEditor.state.selection.to, + editor._tiptapEditor.state.selection.$to.end() + ); + + const fullPrompt = `Below is the content of an editable Markdown blocks. It's split into sections to show where the user selection is located. + +Content before: +\`\`\` +${beforeMarkdown} +\`\`\` +The Markdown content in the blocks before the selected section. + +Selected section: +\`\`\` +${selectedMarkdown} +\`\`\` +The Markdown content of the selected section. + +Content after: +\`\`\` +${afterMarkdown} +\`\`\` +The Markdown content in the blocks after the selected section. + +Text before: +\`\`\` +${beforeText} +\`\`\` +The text between the start of the selected section and the text cursor. + +Text after: +\`\`\` +${afterText} +\`\`\` +The text between the text cursor and the end of the selected section. + +Prompt: +\`\`\` +${prompt} +\`\`\` +The AI prompt provided by the user. + +Provide a modified version of the selected section where content based on the prompt is inserted at the text cursor position. Provide Markdown only, the response should not contain any additional text. +`; + + const aiResponse = await mockAICall(fullPrompt); + const replacementBlocks = await editor.tryParseMarkdownToBlocks(aiResponse); + editor.replaceBlocks([selectedBlock], replacementBlocks); + + const newDocSize = editor._tiptapEditor.state.doc.nodeSize; + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.state.tr.setSelection( + TextSelection.create( + editor._tiptapEditor.state.doc, + endPos, + endPos + newDocSize - oldDocSize + ) + ) + ); + + editor.formattingToolbar.closeMenu(); + editor.focus(); + editor.aiInlineToolbar.open(prompt, "insertAfterSelection"); + + return [selectedBlock]; +}; diff --git a/packages/core/src/blocks/AIBlockContent/mockAIOperation.ts b/packages/core/src/blocks/AIBlockContent/mockAIOperation.ts deleted file mode 100644 index 8eb25293d..000000000 --- a/packages/core/src/blocks/AIBlockContent/mockAIOperation.ts +++ /dev/null @@ -1,263 +0,0 @@ -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { BlockIdentifier } from "../../schema"; - -// Mock function to edit the document using AI. Has 3 operation modes: -// - replaceSelection: Replaces the selected text with AI generated text. To -// make things easier, this actually replaces the selected blocks, and the final -// prompt specifies to only modify the selected text. -// - replaceBlock: Replaces a block with AI generated content. The block has to -// be specified when calling the function. -// - insert: Inserts AI generated text at the text cursor position. Assumes that -// a selection isn't active. To make things easier, this actually replaces the -// block containing the text cursor, and the final prompt specifies to only -// append content to it. -export const mockAIOperation = async ( - editor: BlockNoteEditor, - prompt: string, - options: - | { operation: "replaceSelection" } - | { operation: "replaceBlock"; blockIdentifier: BlockIdentifier } - | { operation: "insert" } = { operation: "insert" } -) => { - const document = editor.document; - - // Un-nests blocks, as we're using Markdown to convert the blocks to a prompt - // for the AI model anyway. - let noNestedBlocks = false; - while (!noNestedBlocks) { - noNestedBlocks = true; - - for (let i = document.length - 1; i >= 0; i--) { - const children = document[i].children; - - if (children.length !== 0) { - noNestedBlocks = false; - document[i].children = []; - document.splice(i + 1, 0, ...children); - } - } - } - - let fullPrompt: string; - - if (options.operation === "replaceSelection") { - const selectedBlocks = editor.getSelection()?.blocks || [ - editor.getTextCursorPosition().block, - ]; - - const firstSelectedBlockIndex = document.findIndex( - (block) => block.id === selectedBlocks[0].id - ); - const lastSelectedBlockIndex = document.findIndex( - (block) => block.id === selectedBlocks[selectedBlocks.length - 1].id - ); - - // We split the document to provide the AI model with full context of what - // exactly is selected. - const before = document.slice(0, firstSelectedBlockIndex); - const selected = document.slice( - firstSelectedBlockIndex, - lastSelectedBlockIndex + 1 - ); - const after = document.slice(lastSelectedBlockIndex + 1); - - const beforeText = editor._tiptapEditor.state.doc.textBetween( - editor._tiptapEditor.state.selection.$from.start(), - editor._tiptapEditor.state.selection.from - ); - const selectedText = editor._tiptapEditor.state.doc.textBetween( - editor._tiptapEditor.state.selection.from, - editor._tiptapEditor.state.selection.to - ); - const afterText = editor._tiptapEditor.state.doc.textBetween( - editor._tiptapEditor.state.selection.to, - editor._tiptapEditor.state.selection.$to.end() - ); - - const beforeMarkdown = await editor.blocksToMarkdownLossy(before); - const selectedMarkdown = await editor.blocksToMarkdownLossy(selected); - const afterMarkdown = await editor.blocksToMarkdownLossy(after); - - fullPrompt = `Below is the content of an editable Markdown document. It's split into sections to show where the user selection is located. - -Content before: -\`\`\` -${beforeMarkdown} -\`\`\` -The Markdown content in the document before the selected section. - -Selected section: -\`\`\` -${selectedMarkdown} -\`\`\` -The Markdown content of the selected section. - -Content after: -\`\`\` -${afterMarkdown} -\`\`\` -The Markdown content in the document after the selected section. - -Text before: -\`\`\` -${beforeText} -\`\`\` -The text between the start of the selected section and the start of the user selection. - -Selected text: -\`\`\` -${selectedText} -\`\`\` -The user selected text within the selected section. - -Text after: -\`\`\` -${afterText} -\`\`\` -The text between the end of the user selection and the end of the selected section. - -Prompt: -\`\`\` -${prompt} -\`\`\` -The AI prompt provided by the user. - -Provide a version of the selected section where the selected text inside is modified based on the prompt. Provide Markdown only, the response should not contain any additional text. -`; - } else if (options.operation === "replaceBlock") { - const blockID = - typeof options.blockIdentifier === "string" - ? options.blockIdentifier - : options.blockIdentifier.id; - - const selectedBlockIndex = document.findIndex( - (block) => block.id === blockID - ); - - // We split the document to provide the AI model with full context of what - // exactly is selected. - const before = document.slice(0, selectedBlockIndex); - const selected = document[selectedBlockIndex]; - const after = document.slice(selectedBlockIndex + 1); - - const beforeMarkdown = await editor.blocksToMarkdownLossy(before); - const selectedMarkdown = await editor.blocksToMarkdownLossy([selected]); - const afterMarkdown = await editor.blocksToMarkdownLossy(after); - - fullPrompt = `Below is the content of an editable Markdown document. It's split into sections to show where the user selection is located. - -Content before: -\`\`\` -${beforeMarkdown} -\`\`\` -The Markdown content in the document before the selected section. - -Selected section: -\`\`\` -${selectedMarkdown} -\`\`\` -The Markdown content of the selected section. - -Content after: -\`\`\` -${afterMarkdown} -\`\`\` -The Markdown content in the document after the selected section. - -Prompt: -\`\`\` -${prompt} -\`\`\` -The AI prompt provided by the user. - -Provide a version of the selected section which is modified based on the prompt. Provide Markdown only, the response should not contain any additional text. -`; - } else { - const selectedBlock = editor.getTextCursorPosition().block; - - const selectedBlockIndex = document.findIndex( - (block) => block.id === selectedBlock.id - ); - - // We split the document to provide the AI model with full context of what - // exactly is selected. - const before = document.slice(0, selectedBlockIndex); - const selected = document[selectedBlockIndex]; - const after = document.slice(selectedBlockIndex + 1); - - const beforeText = editor._tiptapEditor.state.doc.textBetween( - editor._tiptapEditor.state.selection.$from.start(), - editor._tiptapEditor.state.selection.from - ); - const afterText = editor._tiptapEditor.state.doc.textBetween( - editor._tiptapEditor.state.selection.to, - editor._tiptapEditor.state.selection.$to.end() - ); - - const beforeMarkdown = await editor.blocksToMarkdownLossy(before); - const selectedMarkdown = await editor.blocksToMarkdownLossy([selected]); - const afterMarkdown = await editor.blocksToMarkdownLossy(after); - - fullPrompt = `Below is the content of an editable Markdown document. It's split into sections to show where the user selection is located. - -Content before: -\`\`\` -${beforeMarkdown} -\`\`\` -The Markdown content in the document before the selected section. - -Selected section: -\`\`\` -${selectedMarkdown} -\`\`\` -The Markdown content of the selected section. - -Content after: -\`\`\` -${afterMarkdown} -\`\`\` -The Markdown content in the document after the selected section. - -Text before: -\`\`\` -${beforeText} -\`\`\` -The text between the start of the selected section and the text cursor. - -Text after: -\`\`\` -${afterText} -\`\`\` -The text between the text cursor and the end of the selected section. - -Prompt: -\`\`\` -${prompt} -\`\`\` -The AI prompt provided by the user. - -Provide a modified version of the selected section where content based on the prompt is inserted at the text cursor position. Provide Markdown only, the response should not contain any additional text. -`; - } - - console.log(fullPrompt); - - const mockFetchAIResponse = async (_prompt: string) => { - return new Promise((resolve) => { - setTimeout(() => { - // resolve( - // `The text before this block discusses cats, focusing on their domestication and origins in the Near East around 7500 BC. It highlights their classification as a domesticated species within the family Felidae. - // - // The text after this block describes dogs, noting their descent from wolves and their domestication over 14,000 years ago. It emphasizes that dogs were the first species domesticated by humans.` - // ); - resolve( - 'Over time, a variety of different cat breeds have emerged. One of these is the Maine Coon, which is one of the largest domesticated cat breeds. Known for their friendly and sociable nature, Maine Coons are often referred to as "gentle giants." They have a distinctive physical appearance, characterized by their tufted ears, bushy tails, and long, shaggy fur that is well-suited for cold climates. Maine Coons are also intelligent and playful, making them excellent companions for families.\n' - ); - }, 1000); - }); - }; - - const aiResponse = await mockFetchAIResponse(fullPrompt); - - return await editor.tryParseMarkdownToBlocks(aiResponse); -}; diff --git a/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts b/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts index a0e737b87..33eb2b6c4 100644 --- a/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts +++ b/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts @@ -2,13 +2,12 @@ import { isNodeSelection, posToDOMRect } from "@tiptap/core"; import { Plugin, PluginKey, PluginView } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Block } from "../../blocks/defaultBlocks"; import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { EventEmitter } from "../../util/EventEmitter"; export type AIInlineToolbarState = UiElementPosition & { prompt: string; - originalContent: Block[]; + operation: "replaceSelection" | "insertAfterSelection"; }; export class AIInlineToolbarView implements PluginView { @@ -94,7 +93,7 @@ export class AIInlineToolbarView implements PluginView { show: true, referencePos: this.getSelectionBoundingBox(), prompt: pluginState.prompt, - originalContent: pluginState.originalContent, + operation: pluginState.operation, }; this.emitUpdate(); @@ -118,13 +117,13 @@ export class AIInlineToolbarView implements PluginView { this.pmView.root.removeEventListener("scroll", this.scrollHandler, true); } - open(prompt: string, originalContent: Block[]) { + open(prompt: string, operation: "replaceSelection" | "insertAfterSelection") { this.pmView.focus(); this.pmView.dispatch( this.pmView.state.tr.scrollIntoView().setMeta(aiInlineToolbarPluginKey, { open: true, prompt, - originalContent, + operation, }) ); } @@ -167,7 +166,11 @@ export class AIInlineToolbarView implements PluginView { } type AIInlineToolbarPluginState = - | { open: true; prompt: string; originalContent: Block[] } + | { + open: true; + prompt: string; + operation: "replaceSelection" | "insertAfterSelection"; + } | { open: false }; export const aiInlineToolbarPluginKey = new PluginKey("AIInlineToolbarPlugin"); @@ -209,8 +212,11 @@ export class AIInlineToolbarProsemirrorPlugin extends EventEmitter { }); } - public open(prompt: string, originalContent: Block[]) { - this.view?.open(prompt, originalContent); + public open( + prompt: string, + operation: "replaceSelection" | "insertAfterSelection" + ) { + this.view?.open(prompt, operation); } public close() { diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 963a2f4ab..b2a75db92 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -90,17 +90,34 @@ export const ar: Dictionary = { aliases: ["ملف", "تحميل", "تضمين", "وسائط", "رابط"], group: "وسائط", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "الرموز التعبيرية", subtext: "تُستخدم لإدراج رمز تعبيري", aliases: ["رمز تعبيري", "إيموجي", "إيموت", "عاطفة", "وجه"], group: "آخرون", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index bcd9aeaa4..1dd7e1512 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -105,17 +105,34 @@ export const fr: Dictionary = { aliases: ["fichier", "téléverser", "intégrer", "média", "url"], group: "Média", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Emoji", subtext: "Utilisé pour insérer un emoji", aliases: ["emoji", "émoticône", "émotion", "visage"], group: "Autres", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index e1688b4cb..6837eb094 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -98,17 +98,34 @@ export const is: Dictionary = { aliases: ["skrá", "hlaða upp", "fella inn", "miðill", "url"], group: "Miðlar", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Emoji", subtext: "Notað til að setja inn smámynd", aliases: ["emoji", "andlitsávísun", "tilfinningar", "andlit"], group: "Annað", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index 4247983cc..84c620d65 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -125,17 +125,34 @@ export const ja: Dictionary = { aliases: ["file", "upload", "embed", "media", "url", "ファイル"], group: "メディア", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "絵文字", subtext: "絵文字を挿入するために使用します", aliases: ["絵文字", "顔文字", "感情表現", "顔"], group: "その他", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 77d169b4e..0e9088fad 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -109,6 +109,18 @@ export const ko: Dictionary = { aliases: ["file", "upload", "embed", "media", "파일", "url"], group: "미디어", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "이모지", subtext: "이모지 삽입용으로 사용됩니다", @@ -124,11 +136,16 @@ export const ko: Dictionary = { ], group: "기타", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index e7bdabadc..c6df47164 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -100,6 +100,18 @@ export const nl: Dictionary = { aliases: ["bestand", "upload", "insluiten", "media", "url"], group: "Media", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Emoji", subtext: "Gebruikt voor het invoegen van een emoji", @@ -111,11 +123,16 @@ export const nl: Dictionary = { ], group: "Overig", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index ec9a9cf21..9d74ba323 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -90,17 +90,34 @@ export const pl: Dictionary = { aliases: ["plik", "wrzuć", "wstaw", "media", "url"], group: "Media", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Emoji", subtext: "Używane do wstawiania emoji", aliases: ["emoji", "emotka", "wyrażenie emocji", "twarz"], group: "Inne", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 292a91f42..3a4e69718 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -97,17 +97,34 @@ export const pt: Dictionary = { aliases: ["arquivo", "upload", "incorporar", "mídia", "url"], group: "Mídia", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Emoji", subtext: "Usado para inserir um emoji", aliases: ["emoji", "emoticon", "expressão emocional", "rosto"], group: "Outros", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index 0fee25b97..31e69f770 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -132,17 +132,34 @@ export const ru: Dictionary = { aliases: ["file", "upload", "embed", "media", "url", "загрузка", "файл"], group: "Медиа", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Эмодзи", subtext: "Используется для вставки эмодзи", aliases: ["эмодзи", "смайлик", "выражение эмоций", "лицо"], group: "Прочее", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index 6d732e0d3..a05dc9668 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -97,6 +97,18 @@ export const vi: Dictionary = { aliases: ["tep", "tai-len", "nhung", "media", "url"], group: "Phương tiện", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "Biểu tượng cảm xúc", subtext: "Dùng để chèn biểu tượng cảm xúc", @@ -110,11 +122,16 @@ export const vi: Dictionary = { ], group: "Khác", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index a23dbefe2..ba83ab06f 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -130,6 +130,18 @@ export const zh: Dictionary = { aliases: ["文件", "上传", "file", "嵌入", "媒体", "url"], group: "媒体", }, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, emoji: { title: "表情符号", subtext: "用于插入表情符号", @@ -144,11 +156,16 @@ export const zh: Dictionary = { ], group: "其他", }, - ai_block: { - title: "AI Block", - subtext: "Create content using generative AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "Others", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], }, }, placeholders: { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 008a60b08..7c5903e0e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,7 +3,7 @@ export * from "./api/exporters/html/externalHTMLExporter"; export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testUtil"; export * from "./blocks/AIBlockContent/AIBlockContent"; -export * from "./blocks/AIBlockContent/mockAIOperation"; +export * from "./blocks/AIBlockContent/mockAIFunctions"; export * from "./blocks/AudioBlockContent/AudioBlockContent"; export * from "./blocks/FileBlockContent/FileBlockContent"; export * from "./blocks/ImageBlockContent/ImageBlockContent"; @@ -47,4 +47,3 @@ export * from "./extensions/UniqueID/UniqueID"; export * from "./api/exporters/markdown/markdownExporter"; export * from "./api/parsers/html/parseHTML"; export * from "./api/parsers/markdown/parseMarkdown"; -export { mockAIOperation } from "./blocks/AIBlockContent/mockAIOperation"; diff --git a/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx b/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx index 9bdd62fe6..7033a6a11 100644 --- a/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx +++ b/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx @@ -2,7 +2,7 @@ import { aiBlockConfig, BlockSchemaWithBlock, InlineContentSchema, - mockAIOperation, + mockAIReplaceBlockContent, StyleSchema, } from "@blocknote/core"; import { @@ -46,15 +46,11 @@ export const ShowPromptButton = ( event.preventDefault(); props.setUpdating(true); setOpened(false); - editor.updateBlock(editor.getTextCursorPosition().block, { - props: { prompt: currentEditingPrompt }, - content: ( - await mockAIOperation(editor, props.prompt, { - operation: "replaceBlock", - blockIdentifier: editor.getTextCursorPosition().block, - }) - )[0].content as any, - }); + await mockAIReplaceBlockContent( + editor, + currentEditingPrompt, + editor.getTextCursorPosition().block + ); props.setUpdating(false); } }, diff --git a/packages/react/src/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx b/packages/react/src/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx index 2281f5672..f024e334d 100644 --- a/packages/react/src/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx +++ b/packages/react/src/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx @@ -2,7 +2,7 @@ import { aiBlockConfig, BlockSchemaWithBlock, InlineContentSchema, - mockAIOperation, + mockAIReplaceBlockContent, StyleSchema, } from "@blocknote/core"; @@ -37,15 +37,11 @@ export const UpdateButton = ( onClick={async () => { props.setUpdating(true); editor.focus(); - editor.updateBlock(editor.getTextCursorPosition().block, { - props: { prompt: props.prompt }, - content: ( - await mockAIOperation(editor, props.prompt, { - operation: "replaceBlock", - blockIdentifier: editor.getTextCursorPosition().block, - }) - )[0].content as any, - }); + await mockAIReplaceBlockContent( + editor, + props.prompt, + editor.getTextCursorPosition().block + ); props.setUpdating(false); }}> {props.updating diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx b/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx index f6e70fc15..c723405ee 100644 --- a/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx +++ b/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx @@ -1,6 +1,12 @@ -import { ReactNode, useState } from "react"; +import { + mockAIReplaceSelection, + mockAIInsertAfterSelection, + Block, +} from "@blocknote/core"; +import { ReactNode, useEffect, useState } from "react"; import { useComponentsContext } from "../../editor/ComponentsContext"; +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; import { AIInlineToolbarProps } from "./AIInlineToolbarProps"; import { AcceptButton } from "./DefaultButtons/AcceptButton"; import { RetryButton } from "./DefaultButtons/RetryButton"; @@ -8,13 +14,14 @@ import { RevertButton } from "./DefaultButtons/RevertButton"; export const getAIBlockToolbarItems = ( props: AIInlineToolbarProps & { + originalBlocks: Block[]; updating: boolean; setUpdating: (updating: boolean) => void; } ): JSX.Element[] => [ - , - , - , + , + , + , ]; /** @@ -31,12 +38,40 @@ export const AIInlineToolbar = ( ) => { const Components = useComponentsContext()!; - const [updating, setUpdating] = useState(false); + const editor = useBlockNoteEditor(); + + const [originalBlocks, setOriginalBlocks] = useState[]>( + [] + ); + const [updating, setUpdating] = useState(true); + + useEffect(() => { + // TODO: Throws an error when strict mode is active because the target + // blocks couldn't be found. Works fine otherwise. + if (props.operation === "replaceSelection") { + mockAIReplaceSelection(editor, props.prompt).then((originalBlocks) => { + setOriginalBlocks(originalBlocks); + setUpdating(false); + }); + } else { + mockAIInsertAfterSelection(editor, props.prompt).then( + (originalBlocks) => { + setOriginalBlocks(originalBlocks); + setUpdating(false); + } + ); + } + }, []); return ( {props.children || - getAIBlockToolbarItems({ ...props, updating, setUpdating })} + getAIBlockToolbarItems({ + ...props, + originalBlocks, + updating, + setUpdating, + })} ); }; diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx index 6f026f4ea..60e463ae8 100644 --- a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx +++ b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx @@ -41,13 +41,13 @@ export const AIInlineToolbarController = (props: { return null; } - const { prompt, originalContent } = state; + const { prompt, operation } = state; const Component = props.aiToolbar || AIInlineToolbar; return (
- +
); }; diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts index 8ed42f73c..4a25f0e80 100644 --- a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts +++ b/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts @@ -1,6 +1,4 @@ -import { Block } from "@blocknote/core"; - export type AIInlineToolbarProps = { prompt: string; - originalContent: Block[]; + operation: "replaceSelection" | "insertAfterSelection"; }; diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx b/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx index 28dec4114..c59c8c304 100644 --- a/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx +++ b/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx @@ -1,24 +1,14 @@ -import { - aiBlockConfig, - BlockSchemaWithBlock, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; +import { RiCheckFill } from "react-icons/ri"; import { useComponentsContext } from "../../../editor/ComponentsContext"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; import { useDictionary } from "../../../i18n/dictionary"; -import { RiCheckFill } from "react-icons/ri"; export const AcceptButton = () => { const dict = useDictionary(); const Components = useComponentsContext()!; - const editor = useBlockNoteEditor< - BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, - InlineContentSchema, - StyleSchema - >(); + const editor = useBlockNoteEditor(); if (!editor.isEditable) { return null; @@ -30,9 +20,7 @@ export const AcceptButton = () => { icon={} mainTooltip={dict.ai_inline_toolbar.accept} label={dict.ai_inline_toolbar.accept} - onClick={async () => { - editor.aiInlineToolbar.close(); - }} + onClick={() => editor.aiInlineToolbar.close()} /> ); }; diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx index 50b8dd340..071844aa7 100644 --- a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx +++ b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx @@ -1,11 +1,4 @@ -import { - aiBlockConfig, - BlockSchemaWithBlock, - InlineContentSchema, - mockAIOperation, - StyleSchema, -} from "@blocknote/core"; -import { TextSelection } from "prosemirror-state"; +import { mockAIReplaceSelection } from "@blocknote/core"; import { RiLoopLeftFill } from "react-icons/ri"; import { useComponentsContext } from "../../../editor/ComponentsContext"; @@ -22,11 +15,7 @@ export const RetryButton = ( const dict = useDictionary(); const Components = useComponentsContext()!; - const editor = useBlockNoteEditor< - BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, - InlineContentSchema, - StyleSchema - >(); + const editor = useBlockNoteEditor(); if (!editor.isEditable) { return null; @@ -39,40 +28,10 @@ export const RetryButton = ( mainTooltip={dict.ai_inline_toolbar.retry} label={dict.ai_inline_toolbar.retry} onClick={async () => { - editor.focus(); - props.setUpdating(true); - - const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; - const startPos = editor._tiptapEditor.state.selection.from; - const endPos = editor._tiptapEditor.state.selection.to; - const currentContent = editor.getSelection()?.blocks || [ - editor.getTextCursorPosition().block, - ]; - - editor.replaceBlocks( - currentContent, - (await mockAIOperation(editor, props.prompt, { - operation: "replaceSelection", - })) as any[] - ); - - const newDocSize = editor._tiptapEditor.state.doc.nodeSize; - - editor._tiptapEditor.view.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create( - editor._tiptapEditor.state.doc, - startPos, - endPos + newDocSize - oldDocSize - ) - ) - ); - + // editor.focus(); + await mockAIReplaceSelection(editor, props.prompt); props.setUpdating(false); - editor.formattingToolbar.closeMenu(); - editor.focus(); - editor.aiInlineToolbar.open(props.prompt, props.originalContent); }}> {props.updating && dict.ai_inline_toolbar.updating}
diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx index fc83a853f..e023f20f0 100644 --- a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx +++ b/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx @@ -1,26 +1,21 @@ -import { - aiBlockConfig, - BlockSchemaWithBlock, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { TextSelection } from "prosemirror-state"; +import { TextSelection } from "@tiptap/pm/state"; import { RiArrowGoBackFill } from "react-icons/ri"; import { useComponentsContext } from "../../../editor/ComponentsContext"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; import { useDictionary } from "../../../i18n/dictionary"; import { AIInlineToolbarProps } from "../AIInlineToolbarProps"; +import { Block } from "@blocknote/core"; -export const RevertButton = (props: AIInlineToolbarProps) => { +export const RevertButton = ( + props: AIInlineToolbarProps & { + originalBlocks: Block[]; + } +) => { const dict = useDictionary(); const Components = useComponentsContext()!; - const editor = useBlockNoteEditor< - BlockSchemaWithBlock<"ai", typeof aiBlockConfig>, - InlineContentSchema, - StyleSchema - >(); + const editor = useBlockNoteEditor(); if (!editor.isEditable) { return null; @@ -33,15 +28,16 @@ export const RevertButton = (props: AIInlineToolbarProps) => { mainTooltip={dict.ai_inline_toolbar.revert} label={dict.ai_inline_toolbar.revert} onClick={() => { - editor.focus(); + editor.aiInlineToolbar.close(); + const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; const startPos = editor._tiptapEditor.state.selection.from; const endPos = editor._tiptapEditor.state.selection.to; - const replacedContent = editor.getSelection()?.blocks || [ + const replacementBlocks = editor.getSelection()?.blocks || [ editor.getTextCursorPosition().block, ]; - editor.replaceBlocks(replacedContent, props.originalContent as any[]); + editor.replaceBlocks(replacementBlocks, props.originalBlocks as any[]); const newDocSize = editor._tiptapEditor.state.doc.nodeSize; @@ -55,9 +51,7 @@ export const RevertButton = (props: AIInlineToolbarProps) => { ) ); - editor.formattingToolbar.closeMenu(); editor.focus(); - editor.aiInlineToolbar.close(); }} /> ); diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx index 710aef58e..87e6741f7 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx @@ -1,16 +1,10 @@ -import { - BlockSchema, - InlineContentSchema, - mockAIOperation, - StyleSchema, -} from "@blocknote/core"; -import { TextSelection } from "@tiptap/pm/state"; +import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react"; import { RiSparkling2Fill } from "react-icons/ri"; -import { useDictionary } from "../../../i18n/dictionary"; import { useComponentsContext } from "../../../editor/ComponentsContext"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../i18n/dictionary"; export const AIButton = () => { const dict = useDictionary(); @@ -23,42 +17,11 @@ export const AIButton = () => { >(); const [currentEditingPrompt, setCurrentEditingPrompt] = useState(""); - const [updating, setUpdating] = useState(false); const runAIEdit = useCallback( async (prompt: string) => { - setUpdating(true); - - const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; - const startPos = editor._tiptapEditor.state.selection.from; - const endPos = editor._tiptapEditor.state.selection.to; - const originalContent = editor.getSelection()?.blocks || [ - editor.getTextCursorPosition().block, - ]; - - editor.replaceBlocks( - originalContent, - (await mockAIOperation(editor, prompt, { - operation: "replaceSelection", - })) as any[] - ); - - const newDocSize = editor._tiptapEditor.state.doc.nodeSize; - - editor._tiptapEditor.view.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create( - editor._tiptapEditor.state.doc, - startPos, - endPos + newDocSize - oldDocSize - ) - ) - ); - - setUpdating(false); editor.formattingToolbar.closeMenu(); - editor.focus(); - editor.aiInlineToolbar.open(prompt, originalContent); + editor.aiInlineToolbar.open(prompt, "replaceSelection"); }, [editor] ); @@ -90,9 +53,8 @@ export const AIButton = () => { className={"bn-button"} label={dict.formatting_toolbar.ai.tooltip} mainTooltip={dict.formatting_toolbar.ai.tooltip} - icon={!updating && }> - {updating && "Updating..."} - + icon={} + /> diff --git a/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx index 52f5b5910..2dd4de630 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx @@ -2,11 +2,9 @@ import { BlockNoteEditor, BlockSchema, InlineContentSchema, - mockAIOperation, StyleSchema, } from "@blocknote/core"; import { DefaultReactSuggestionItem } from "./types"; -import { TextSelection } from "@tiptap/pm/state"; // TODO: Maybe we don't want to define the default AI prompts based on the // dictionary @@ -21,41 +19,12 @@ export function getDefaultAIMenuItems< return Object.values(editor.dictionary.ai_menu).map((item) => ({ ...item, onItemClick: async () => { - const oldDocSize = editor._tiptapEditor.state.doc.nodeSize; - const startPos = editor._tiptapEditor.state.selection.from; - const endPos = editor._tiptapEditor.state.selection.to; - const originalContent = editor.getSelection()?.blocks || [ - editor.getTextCursorPosition().block, - ]; - - const prompt = - item.title === editor.dictionary.ai_menu.custom_prompt.title + editor.aiInlineToolbar.open( + editor.dictionary.ai_menu.custom_prompt.title === item.title ? query - : item.title; - - editor.replaceBlocks( - originalContent, - (await mockAIOperation(editor, prompt, { - operation: "replaceBlock", - blockIdentifier: editor.getTextCursorPosition().block, - })) as any[] - ); - - const newDocSize = editor._tiptapEditor.state.doc.nodeSize; - - editor._tiptapEditor.view.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create( - editor._tiptapEditor.state.doc, - startPos, - endPos + newDocSize - oldDocSize - ) - ) + : item.title, + "insertAfterSelection" ); - - editor.formattingToolbar.closeMenu(); - editor.focus(); - editor.aiInlineToolbar.open(prompt, originalContent); }, })); } From 5c66cfe8f6634ccce9e63418c091f05f758f36ca Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Mon, 9 Sep 2024 15:29:05 +0200 Subject: [PATCH 05/45] Extracted AI to separate package & changed AI block toolbar UX --- examples/01-basic/01-minimal/App.tsx | 5 +- packages/ai/package.json | 88 +++++++++++++++++++ .../blocks/AIBlockContent/AIBlockContent.ts | 16 ++-- .../blocks/AIBlockContent/mockAIFunctions.ts | 11 +-- .../src/core/blocks/defaultBlockTypeGuards.ts | 18 ++++ packages/ai/src/core/blocks/defaultBlocks.ts | 38 ++++++++ .../ai/src/core/editor/BlockNoteContext.ts | 42 +++++++++ .../ai/src/core/editor/BlockNoteEditor.ts | 67 ++++++++++++++ packages/ai/src/core/editor/style.css | 44 ++++++++++ .../AIBlockToolbar/AIBlockToolbarPlugin.ts | 10 ++- .../AIInlineToolbar/AIInlineToolbarPlugin.ts | 4 +- .../getDefaultSlashMenuItems.ts | 40 +++++++++ packages/ai/src/core/index.ts | 11 +++ packages/ai/src/index.ts | 3 + packages/ai/src/mantine/index.tsx | 28 ++++++ .../AIBlockToolbar/AIBlockToolbar.tsx | 2 +- .../AIBlockToolbarController.tsx | 3 +- .../AIBlockToolbar/AIBlockToolbarProps.ts | 0 .../DefaultButtons/ShowPromptButton.tsx | 23 +++-- .../DefaultButtons/UpdateButton.tsx | 7 +- .../AIInlineToolbar/AIInlineToolbar.tsx | 16 ++-- .../AIInlineToolbarController.tsx | 4 +- .../AIInlineToolbar/AIInlineToolbarProps.ts | 0 .../DefaultButtons/AcceptButton.tsx | 3 +- .../DefaultButtons/RetryButton.tsx | 5 +- .../DefaultButtons/RevertButton.tsx | 7 +- .../DefaultButtons/AIButton.tsx | 3 +- .../DefaultSelects/BlockTypeSelect.tsx | 44 ++++++++++ .../FormattingToolbar/FormattingToolbar.tsx | 44 ++++++++++ .../FormattingToolbarController.tsx | 16 ++++ .../SuggestionMenu/getDefaultAIMenuItems.tsx | 12 ++- .../getDefaultReactSlashMenuItems.tsx | 50 +++++++++++ .../src/react/editor/BlockNoteDefaultUI.tsx | 56 ++++++++++++ .../ai/src/react/editor/BlockNoteView.tsx | 42 +++++++++ packages/ai/src/react/editor/style.css | 3 + .../ai/src/react/hooks/useBlockNoteEditor.ts | 21 +++++ .../ai/src/react/hooks/useCreateBlockNote.tsx | 34 +++++++ packages/ai/src/react/index.ts | 26 ++++++ packages/ai/src/style.css | 3 + packages/ai/tsconfig.json | 32 +++++++ packages/ai/vite.config.ts | 53 +++++++++++ packages/core/src/blocks/defaultBlocks.ts | 2 - packages/core/src/editor/BlockNoteEditor.ts | 15 +--- .../SuggestionMenu/DefaultSuggestionItem.ts | 2 +- .../getDefaultSlashMenuItems.ts | 44 +++------- packages/core/src/i18n/locales/ar.ts | 1 + packages/core/src/i18n/locales/en.ts | 3 +- packages/core/src/i18n/locales/fr.ts | 1 + packages/core/src/i18n/locales/is.ts | 1 + packages/core/src/i18n/locales/ja.ts | 1 + packages/core/src/i18n/locales/ko.ts | 1 + packages/core/src/i18n/locales/nl.ts | 1 + packages/core/src/i18n/locales/pl.ts | 1 + packages/core/src/i18n/locales/pt.ts | 1 + packages/core/src/i18n/locales/ru.ts | 1 + packages/core/src/i18n/locales/vi.ts | 1 + packages/core/src/i18n/locales/zh.ts | 1 + packages/core/src/index.ts | 4 +- packages/mantine/src/index.tsx | 9 +- packages/mantine/src/style.css | 6 ++ .../mantine/src/toolbar/ToolbarButton.tsx | 1 + .../DefaultSelects/BlockTypeSelect.tsx | 7 -- .../FormattingToolbar/FormattingToolbar.tsx | 2 - .../getDefaultReactSlashMenuItems.tsx | 6 +- .../src/components/SuggestionMenu/types.tsx | 2 +- .../react/src/editor/BlockNoteDefaultUI.tsx | 19 ---- packages/react/src/editor/BlockNoteView.tsx | 2 +- packages/react/src/editor/styles.css | 6 +- packages/react/src/index.ts | 2 + playground/package.json | 1 + playground/tsconfig.json | 1 + playground/vite.config.ts | 1 + 72 files changed, 926 insertions(+), 154 deletions(-) create mode 100644 packages/ai/package.json rename packages/{core/src => ai/src/core}/blocks/AIBlockContent/AIBlockContent.ts (94%) rename packages/{core/src => ai/src/core}/blocks/AIBlockContent/mockAIFunctions.ts (97%) create mode 100644 packages/ai/src/core/blocks/defaultBlockTypeGuards.ts create mode 100644 packages/ai/src/core/blocks/defaultBlocks.ts create mode 100644 packages/ai/src/core/editor/BlockNoteContext.ts create mode 100644 packages/ai/src/core/editor/BlockNoteEditor.ts create mode 100644 packages/ai/src/core/editor/style.css rename packages/{core/src => ai/src/core}/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts (95%) rename packages/{core/src => ai/src/core}/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts (97%) create mode 100644 packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts create mode 100644 packages/ai/src/core/index.ts create mode 100644 packages/ai/src/index.ts create mode 100644 packages/ai/src/mantine/index.tsx rename packages/{react/src => ai/src/react}/components/AIBlockToolbar/AIBlockToolbar.tsx (94%) rename packages/{react/src => ai/src/react}/components/AIBlockToolbar/AIBlockToolbarController.tsx (90%) rename packages/{react/src => ai/src/react}/components/AIBlockToolbar/AIBlockToolbarProps.ts (100%) rename packages/{react/src => ai/src/react}/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx (78%) rename packages/{react/src => ai/src/react}/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx (82%) rename packages/{react/src => ai/src/react}/components/AIInlineToolbar/AIInlineToolbar.tsx (90%) rename packages/{react/src => ai/src/react}/components/AIInlineToolbar/AIInlineToolbarController.tsx (90%) rename packages/{react/src => ai/src/react}/components/AIInlineToolbar/AIInlineToolbarProps.ts (100%) rename packages/{react/src => ai/src/react}/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx (82%) rename packages/{react/src => ai/src/react}/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx (84%) rename packages/{react/src => ai/src/react}/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx (89%) rename packages/{react/src => ai/src/react}/components/FormattingToolbar/DefaultButtons/AIButton.tsx (94%) create mode 100644 packages/ai/src/react/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx create mode 100644 packages/ai/src/react/components/FormattingToolbar/FormattingToolbar.tsx create mode 100644 packages/ai/src/react/components/FormattingToolbar/FormattingToolbarController.tsx rename packages/{react/src => ai/src/react}/components/SuggestionMenu/getDefaultAIMenuItems.tsx (71%) create mode 100644 packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx create mode 100644 packages/ai/src/react/editor/BlockNoteDefaultUI.tsx create mode 100644 packages/ai/src/react/editor/BlockNoteView.tsx create mode 100644 packages/ai/src/react/editor/style.css create mode 100644 packages/ai/src/react/hooks/useBlockNoteEditor.ts create mode 100644 packages/ai/src/react/hooks/useCreateBlockNote.tsx create mode 100644 packages/ai/src/react/index.ts create mode 100644 packages/ai/src/style.css create mode 100644 packages/ai/tsconfig.json create mode 100644 packages/ai/vite.config.ts diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index c545b7b4d..a302d17a8 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -1,7 +1,8 @@ import "@blocknote/core/fonts/inter.css"; -import { useCreateBlockNote } from "@blocknote/react"; -import { BlockNoteView } from "@blocknote/mantine"; +import { useCreateBlockNote, BlockNoteView } from "@blocknote/ai"; import "@blocknote/mantine/style.css"; +// import { useCreateBlockNote } from "@blocknote/react"; +// import { BlockNoteView } from "@blocknote/mantine"; export default function App() { // Creates a new editor instance. diff --git a/packages/ai/package.json b/packages/ai/package.json new file mode 100644 index 000000000..57e143d97 --- /dev/null +++ b/packages/ai/package.json @@ -0,0 +1,88 @@ +{ + "name": "@blocknote/ai", + "homepage": "https://github.com/TypeCellOS/BlockNote", + "private": false, + "license": "MPL-2.0", + "version": "0.15.3", + "files": [ + "dist", + "types", + "src" + ], + "keywords": [ + "react", + "javascript", + "editor", + "typescript", + "prosemirror", + "wysiwyg", + "rich-text-editor", + "notion", + "yjs", + "block-based", + "tiptap" + ], + "description": "A \"Notion-style\" block-based extensible text editor built on top of Prosemirror and Tiptap.", + "type": "module", + "source": "src/index.ts", + "types": "./types/src/index.d.ts", + "main": "./dist/blocknote-ai.umd.cjs", + "module": "./dist/blocknote-ai.js", + "exports": { + ".": { + "types": "./types/src/index.d.ts", + "import": "./dist/blocknote-ai.js", + "require": "./dist/blocknote-ai.umd.cjs" + }, + "./style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" + } + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release", + "preview": "vite preview", + "lint": "eslint src --max-warnings 0", + "clean": "rimraf dist && rimraf types" + }, + "dependencies": { + "@blocknote/core": "^0.15.3", + "@blocknote/mantine": "^0.15.3", + "@blocknote/react": "^0.15.3", + "@floating-ui/react": "^0.26.4", + "@tiptap/core": "^2.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-view": "^1.33.7", + "react": "^18", + "react-dom": "^18", + "react-icons": "^5.2.1" + }, + "devDependencies": { + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.10.0", + "prettier": "^2.7.1", + "rimraf": "^5.0.5", + "rollup-plugin-webpack-stats": "^0.2.2", + "typescript": "^5.3.3", + "vite": "^5.3.4", + "vite-plugin-eslint": "^1.8.1", + "vite-plugin-externalize-deps": "^0.8.0" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18" + }, + "eslintConfig": { + "extends": [ + "../../.eslintrc.js" + ] + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts b/packages/ai/src/core/blocks/AIBlockContent/AIBlockContent.ts similarity index 94% rename from packages/core/src/blocks/AIBlockContent/AIBlockContent.ts rename to packages/ai/src/core/blocks/AIBlockContent/AIBlockContent.ts index afd4b3495..8edfa064e 100644 --- a/packages/core/src/blocks/AIBlockContent/AIBlockContent.ts +++ b/packages/ai/src/core/blocks/AIBlockContent/AIBlockContent.ts @@ -1,11 +1,12 @@ -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockConfig, + PropSchema, + defaultProps, BlockFromConfig, createBlockSpec, - PropSchema, -} from "../../schema"; -import { defaultProps } from "../defaultProps"; +} from "@blocknote/core"; + +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { mockAIReplaceBlockContent } from "./mockAIFunctions"; export const aiPropSchema = { @@ -13,6 +14,9 @@ export const aiPropSchema = { prompt: { default: "" as const, }, + timeGenerated: { + default: 0 as const, + }, } satisfies PropSchema; export const aiBlockConfig = { @@ -31,7 +35,7 @@ export const aiRender = ( // TODO: Updating text content in this way isn't working generateButton.textContent = "Generating..."; - mockAIReplaceBlockContent(editor, prompt, block.id); + mockAIReplaceBlockContent(editor, prompt, block); }; const promptBox = document.createElement("div"); @@ -106,6 +110,6 @@ export const aiToExternalHTML = ( }; export const AIBlock = createBlockSpec(aiBlockConfig, { - render: aiRender, + render: aiRender as any, // TODO? toExternalHTML: aiToExternalHTML, }); diff --git a/packages/core/src/blocks/AIBlockContent/mockAIFunctions.ts b/packages/ai/src/core/blocks/AIBlockContent/mockAIFunctions.ts similarity index 97% rename from packages/core/src/blocks/AIBlockContent/mockAIFunctions.ts rename to packages/ai/src/core/blocks/AIBlockContent/mockAIFunctions.ts index bf74b4272..0824e32c1 100644 --- a/packages/core/src/blocks/AIBlockContent/mockAIFunctions.ts +++ b/packages/ai/src/core/blocks/AIBlockContent/mockAIFunctions.ts @@ -1,8 +1,7 @@ -import { TextSelection } from "@tiptap/pm/state"; +import { Block, BlockIdentifier } from "@blocknote/core"; +import { TextSelection } from "prosemirror-state"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import type { BlockIdentifier } from "../../schema"; -import type { Block } from "../defaultBlocks"; const flattenBlocks = ( blocks: Block[] @@ -220,13 +219,11 @@ Replace the selected section with content based on the prompt. Provide Markdown const aiResponse = await mockAICall(fullPrompt); const replacementBlocks = await editor.tryParseMarkdownToBlocks(aiResponse); - editor.updateBlock(editor.getTextCursorPosition().block, { + editor.updateBlock(selectedBlock, { props: { prompt }, content: replacementBlocks[0].content, }); - - // TODO: Selection update - + editor.setTextCursorPosition(selectedBlock, "end"); editor.focus(); return [selectedBlock]; diff --git a/packages/ai/src/core/blocks/defaultBlockTypeGuards.ts b/packages/ai/src/core/blocks/defaultBlockTypeGuards.ts new file mode 100644 index 000000000..7df8f297b --- /dev/null +++ b/packages/ai/src/core/blocks/defaultBlockTypeGuards.ts @@ -0,0 +1,18 @@ +import { InlineContentSchema, StyleSchema } from "@blocknote/core"; + +import type { BlockNoteEditor } from "../editor/BlockNoteEditor"; +import { defaultBlockSchema, DefaultBlockSchema } from "./defaultBlocks"; + +export function checkDefaultBlockTypeInSchema< + BlockType extends keyof DefaultBlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + blockType: BlockType, + editor: BlockNoteEditor +): editor is BlockNoteEditor<{ Type: DefaultBlockSchema[BlockType] }, I, S> { + return ( + blockType in editor.schema.blockSchema && + editor.schema.blockSchema[blockType] === defaultBlockSchema[blockType] + ); +} diff --git a/packages/ai/src/core/blocks/defaultBlocks.ts b/packages/ai/src/core/blocks/defaultBlocks.ts new file mode 100644 index 000000000..549ade3f3 --- /dev/null +++ b/packages/ai/src/core/blocks/defaultBlocks.ts @@ -0,0 +1,38 @@ +import { + BlockNoDefaults, + BlockSchema, + BlockSpecs, + InlineContentSchema, + PartialBlockNoDefaults, + StyleSchema, + getBlockSchemaFromSpecs, + defaultBlockSpecs as defaultCoreBlockSpecs, + DefaultInlineContentSchema, + DefaultStyleSchema, +} from "@blocknote/core"; + +import { AIBlock } from "./AIBlockContent/AIBlockContent"; + +export const defaultBlockSpecs = { + ...defaultCoreBlockSpecs, + ai: AIBlock, +} satisfies BlockSpecs; + +export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs); + +// underscore is used that in case a user overrides DefaultBlockSchema, +// they can still access the original default block schema +export type _DefaultBlockSchema = typeof defaultBlockSchema; +export type DefaultBlockSchema = _DefaultBlockSchema; + +export type PartialBlock< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +> = PartialBlockNoDefaults; + +export type Block< + BSchema extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema +> = BlockNoDefaults; diff --git a/packages/ai/src/core/editor/BlockNoteContext.ts b/packages/ai/src/core/editor/BlockNoteContext.ts new file mode 100644 index 000000000..ed5a0d8b6 --- /dev/null +++ b/packages/ai/src/core/editor/BlockNoteContext.ts @@ -0,0 +1,42 @@ +import { + BlockNoteSchema, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { createContext, useContext, useState } from "react"; + +import type { BlockNoteEditor } from "./BlockNoteEditor"; + +type BlockNoteContextValue< + BSchema extends BlockSchema = DefaultBlockSchema, + ISchema extends InlineContentSchema = DefaultInlineContentSchema, + SSchema extends StyleSchema = DefaultStyleSchema +> = { + setContentEditableProps?: ReturnType>>[1]; // copy type of setXXX from useState + editor?: BlockNoteEditor; + colorSchemePreference?: "light" | "dark"; +}; + +export const BlockNoteContext = createContext< + BlockNoteContextValue | undefined +>(undefined); + +/** + * Get the BlockNoteContext instance from the nearest BlockNoteContext provider + * @param _schema: optional, pass in the schema to return type-safe Context if you're using a custom schema + */ +export function useBlockNoteContext< + BSchema extends BlockSchema = DefaultBlockSchema, + ISchema extends InlineContentSchema = DefaultInlineContentSchema, + SSchema extends StyleSchema = DefaultStyleSchema +>( + _schema?: BlockNoteSchema +): BlockNoteContextValue | undefined { + const context = useContext(BlockNoteContext) as any; + + return context; +} diff --git a/packages/ai/src/core/editor/BlockNoteEditor.ts b/packages/ai/src/core/editor/BlockNoteEditor.ts new file mode 100644 index 000000000..f8b7aa225 --- /dev/null +++ b/packages/ai/src/core/editor/BlockNoteEditor.ts @@ -0,0 +1,67 @@ +import { + BlockNoteEditor as BlockNoteCoreEditor, + BlockNoteEditorOptions, + BlockNoteSchema, + BlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { Extension } from "@tiptap/core"; + +import { DefaultBlockSchema, defaultBlockSpecs } from "../blocks/defaultBlocks"; +import { AIBlockToolbarProsemirrorPlugin } from "../extensions/AIBlockToolbar/AIBlockToolbarPlugin"; +import { AIInlineToolbarProsemirrorPlugin } from "../extensions/AIInlineToolbar/AIInlineToolbarPlugin"; +// import { checkDefaultBlockTypeInSchema } from "../blocks/defaultBlockTypeGuards"; + +export class BlockNoteEditor< + BSchema extends BlockSchema = DefaultBlockSchema, + ISchema extends InlineContentSchema = DefaultInlineContentSchema, + SSchema extends StyleSchema = DefaultStyleSchema +> extends BlockNoteCoreEditor { + public readonly aiBlockToolbar?: AIBlockToolbarProsemirrorPlugin = + new AIBlockToolbarProsemirrorPlugin(); + public readonly aiInlineToolbar: AIInlineToolbarProsemirrorPlugin = + new AIInlineToolbarProsemirrorPlugin(); + + protected constructor( + protected readonly options: Partial> + ) { + super({ + schema: BlockNoteSchema.create({ + blockSpecs: defaultBlockSpecs, + }), + ...options, + _tiptapOptions: { + extensions: [ + Extension.create({ + name: "BlockNoteAIUIExtension", + + addProseMirrorPlugins: () => { + return [ + ...(this.aiBlockToolbar ? [this.aiBlockToolbar.plugin] : []), + this.aiInlineToolbar.plugin, + ]; + }, + }), + ], + ...options._tiptapOptions, + }, + }); + + // if (checkDefaultBlockTypeInSchema("ai", this)) { + // this.aiBlockToolbar = new AIBlockToolbarProsemirrorPlugin(); + // } + // + // this.aiInlineToolbar = new AIInlineToolbarProsemirrorPlugin(); + } + + public static create< + BSchema extends BlockSchema = DefaultBlockSchema, + ISchema extends InlineContentSchema = DefaultInlineContentSchema, + SSchema extends StyleSchema = DefaultStyleSchema + >(options: Partial> = {}) { + return new BlockNoteEditor(options); + } +} diff --git a/packages/ai/src/core/editor/style.css b/packages/ai/src/core/editor/style.css new file mode 100644 index 000000000..d193fc2be --- /dev/null +++ b/packages/ai/src/core/editor/style.css @@ -0,0 +1,44 @@ +/* AI Block */ +[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; +} + +.bn-editor[contenteditable="true"] +[data-content-type="ai"][data-prompt] p:hover { + outline: solid 3px rgba(154, 56, 173, 0.1); +} + +.bn-editor[contenteditable="true"] +[data-content-type="ai"][data-prompt][data-is-focused] p { + outline: solid 3px rgba(154, 56, 173, 0.2); +} \ No newline at end of file diff --git a/packages/core/src/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts b/packages/ai/src/core/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts similarity index 95% rename from packages/core/src/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts rename to packages/ai/src/core/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts index d15adcd7c..98a41c301 100644 --- a/packages/core/src/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts +++ b/packages/ai/src/core/extensions/AIBlockToolbar/AIBlockToolbarPlugin.ts @@ -1,10 +1,12 @@ +import { + BlockInfo, + UiElementPosition, + getBlockInfoFromPos, + EventEmitter, +} from "@blocknote/core"; import { Plugin, PluginKey, PluginView } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { BlockInfo, getBlockInfoFromPos } from "../../api/getBlockInfoFromPos"; -import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; -import { EventEmitter } from "../../util/EventEmitter"; - export type AIBlockToolbarState = UiElementPosition & { prompt?: string }; export class AIBlockToolbarView implements PluginView { diff --git a/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts b/packages/ai/src/core/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts similarity index 97% rename from packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts rename to packages/ai/src/core/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts index 33eb2b6c4..daca532b7 100644 --- a/packages/core/src/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts +++ b/packages/ai/src/core/extensions/AIInlineToolbar/AIInlineToolbarPlugin.ts @@ -1,10 +1,8 @@ +import { EventEmitter, UiElementPosition } from "@blocknote/core"; import { isNodeSelection, posToDOMRect } from "@tiptap/core"; import { Plugin, PluginKey, PluginView } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; -import { EventEmitter } from "../../util/EventEmitter"; - export type AIInlineToolbarState = UiElementPosition & { prompt: string; operation: "replaceSelection" | "insertAfterSelection"; diff --git a/packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts new file mode 100644 index 000000000..08b10c2fd --- /dev/null +++ b/packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -0,0 +1,40 @@ +import { + DefaultSuggestionItem, + insertOrUpdateBlock, + getDefaultSlashMenuItems as getDefaultCoreSlashMenuItems, + BlockSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; + +import { checkDefaultBlockTypeInSchema } from "../../blocks/defaultBlockTypeGuards"; +import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; + +export function getDefaultSlashMenuItems< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>(editor: BlockNoteEditor) { + const items: DefaultSuggestionItem[] = getDefaultCoreSlashMenuItems(editor); + const insertionIndex = items.findIndex((item) => item.key === "emoji"); + + items.splice(insertionIndex, 0, { + onItemClick: () => editor.openSelectionMenu("`"), + key: "ai", + ...editor.dictionary.slash_menu.ai, + }); + + if (checkDefaultBlockTypeInSchema("ai", editor)) { + items.splice(insertionIndex, 0, { + onItemClick: () => { + insertOrUpdateBlock(editor, { + type: "ai", + }); + }, + key: "ai_block", + ...editor.dictionary.slash_menu.ai_block, + }); + } + + return items; +} diff --git a/packages/ai/src/core/index.ts b/packages/ai/src/core/index.ts new file mode 100644 index 000000000..7c5a1efa6 --- /dev/null +++ b/packages/ai/src/core/index.ts @@ -0,0 +1,11 @@ +export * from "./blocks/AIBlockContent/AIBlockContent"; +export * from "./blocks/AIBlockContent/mockAIFunctions"; +export * from "./blocks/defaultBlocks"; +export * from "./blocks/defaultBlockTypeGuards"; + +export * from "./editor/BlockNoteContext"; +export * from "./editor/BlockNoteEditor"; + +export * from "./extensions/AIBlockToolbar/AIBlockToolbarPlugin"; +export * from "./extensions/AIInlineToolbar/AIInlineToolbarPlugin"; +export * from "./extensions/SuggestionMenu/getDefaultSlashMenuItems"; diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts new file mode 100644 index 000000000..1863491c8 --- /dev/null +++ b/packages/ai/src/index.ts @@ -0,0 +1,3 @@ +export * from "./core"; +export * from "./mantine"; +export * from "./react"; diff --git a/packages/ai/src/mantine/index.tsx b/packages/ai/src/mantine/index.tsx new file mode 100644 index 000000000..9579e5684 --- /dev/null +++ b/packages/ai/src/mantine/index.tsx @@ -0,0 +1,28 @@ +import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { ComponentProps } from "react"; +import { + BlockNoteView as BlockNoteViewMantine, + Theme, +} from "@blocknote/mantine"; + +import { BlockNoteViewRaw } from "../react"; + +export const BlockNoteView = < + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema +>( + props: Omit< + ComponentProps>, + "theme" + > & { + theme?: + | "light" + | "dark" + | Theme + | { + light: Theme; + dark: Theme; + }; + } +) => ; diff --git a/packages/react/src/components/AIBlockToolbar/AIBlockToolbar.tsx b/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbar.tsx similarity index 94% rename from packages/react/src/components/AIBlockToolbar/AIBlockToolbar.tsx rename to packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbar.tsx index 33dd7bc09..eb75c65c4 100644 --- a/packages/react/src/components/AIBlockToolbar/AIBlockToolbar.tsx +++ b/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbar.tsx @@ -1,6 +1,6 @@ +import { useComponentsContext } from "@blocknote/react"; import { ReactNode, useState } from "react"; -import { useComponentsContext } from "../../editor/ComponentsContext"; import { AIBlockToolbarProps } from "./AIBlockToolbarProps"; import { ShowPromptButton } from "./DefaultButtons/ShowPromptButton"; import { UpdateButton } from "./DefaultButtons/UpdateButton"; diff --git a/packages/react/src/components/AIBlockToolbar/AIBlockToolbarController.tsx b/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbarController.tsx similarity index 90% rename from packages/react/src/components/AIBlockToolbar/AIBlockToolbarController.tsx rename to packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbarController.tsx index 83b84c648..d77a2820d 100644 --- a/packages/react/src/components/AIBlockToolbar/AIBlockToolbarController.tsx +++ b/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbarController.tsx @@ -1,10 +1,9 @@ import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { useUIElementPositioning, useUIPluginState } from "@blocknote/react"; import { flip, offset } from "@floating-ui/react"; import { FC } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; -import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; -import { useUIPluginState } from "../../hooks/useUIPluginState"; import { AIBlockToolbar } from "./AIBlockToolbar"; import { AIBlockToolbarProps } from "./AIBlockToolbarProps"; diff --git a/packages/react/src/components/AIBlockToolbar/AIBlockToolbarProps.ts b/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbarProps.ts similarity index 100% rename from packages/react/src/components/AIBlockToolbar/AIBlockToolbarProps.ts rename to packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbarProps.ts diff --git a/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx b/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx similarity index 78% rename from packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx rename to packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx index 7033a6a11..7f795c493 100644 --- a/packages/react/src/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx +++ b/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx @@ -1,10 +1,9 @@ import { - aiBlockConfig, BlockSchemaWithBlock, InlineContentSchema, - mockAIReplaceBlockContent, StyleSchema, } from "@blocknote/core"; +import { useComponentsContext, useDictionary } from "@blocknote/react"; import { ChangeEvent, KeyboardEvent, @@ -14,9 +13,9 @@ import { } from "react"; import { RiSparkling2Fill } from "react-icons/ri"; -import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { aiBlockConfig } from "../../../../core/blocks/AIBlockContent/AIBlockContent"; +import { mockAIReplaceBlockContent } from "../../../../core/blocks/AIBlockContent/mockAIFunctions"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; -import { useDictionary } from "../../../i18n/dictionary"; import { AIBlockToolbarProps } from "../AIBlockToolbarProps"; export const ShowPromptButton = ( @@ -81,7 +80,19 @@ export const ShowPromptButton = ( } label={dict.ai_block_toolbar.show_prompt} onClick={handleClick}> {dict.ai_block_toolbar.show_prompt} @@ -92,7 +103,7 @@ export const ShowPromptButton = ( variant={"form-popover"}> } value={currentEditingPrompt || ""} autoFocus={true} diff --git a/packages/react/src/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx b/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx similarity index 82% rename from packages/react/src/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx rename to packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx index f024e334d..a618c7087 100644 --- a/packages/react/src/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx +++ b/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/UpdateButton.tsx @@ -1,14 +1,13 @@ import { - aiBlockConfig, BlockSchemaWithBlock, InlineContentSchema, - mockAIReplaceBlockContent, StyleSchema, } from "@blocknote/core"; +import { useComponentsContext, useDictionary } from "@blocknote/react"; -import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { aiBlockConfig } from "../../../../core/blocks/AIBlockContent/AIBlockContent"; +import { mockAIReplaceBlockContent } from "../../../../core/blocks/AIBlockContent/mockAIFunctions"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; -import { useDictionary } from "../../../i18n/dictionary"; import { AIBlockToolbarProps } from "../AIBlockToolbarProps"; export const UpdateButton = ( diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx b/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbar.tsx similarity index 90% rename from packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx rename to packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbar.tsx index c723405ee..1454be673 100644 --- a/packages/react/src/components/AIInlineToolbar/AIInlineToolbar.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbar.tsx @@ -1,18 +1,18 @@ -import { - mockAIReplaceSelection, - mockAIInsertAfterSelection, - Block, -} from "@blocknote/core"; +import { Block } from "@blocknote/core"; +import { useComponentsContext } from "@blocknote/react"; import { ReactNode, useEffect, useState } from "react"; -import { useComponentsContext } from "../../editor/ComponentsContext"; +import { + mockAIInsertAfterSelection, + mockAIReplaceSelection, +} from "../../../core/blocks/AIBlockContent/mockAIFunctions"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; import { AIInlineToolbarProps } from "./AIInlineToolbarProps"; import { AcceptButton } from "./DefaultButtons/AcceptButton"; import { RetryButton } from "./DefaultButtons/RetryButton"; import { RevertButton } from "./DefaultButtons/RevertButton"; -export const getAIBlockToolbarItems = ( +export const getAIInlineToolbarItems = ( props: AIInlineToolbarProps & { originalBlocks: Block[]; updating: boolean; @@ -66,7 +66,7 @@ export const AIInlineToolbar = ( return ( {props.children || - getAIBlockToolbarItems({ + getAIInlineToolbarItems({ ...props, originalBlocks, updating, diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx b/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbarController.tsx similarity index 90% rename from packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx rename to packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbarController.tsx index 60e463ae8..281e08793 100644 --- a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarController.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbarController.tsx @@ -1,10 +1,10 @@ import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { useUIElementPositioning, useUIPluginState } from "@blocknote/react"; import { flip, offset } from "@floating-ui/react"; import { FC } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; -import { useUIElementPositioning } from "../../hooks/useUIElementPositioning"; -import { useUIPluginState } from "../../hooks/useUIPluginState"; + import { AIInlineToolbar } from "./AIInlineToolbar"; import { AIInlineToolbarProps } from "./AIInlineToolbarProps"; diff --git a/packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts b/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbarProps.ts similarity index 100% rename from packages/react/src/components/AIInlineToolbar/AIInlineToolbarProps.ts rename to packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbarProps.ts diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx similarity index 82% rename from packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx rename to packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx index c59c8c304..8a8644bb5 100644 --- a/packages/react/src/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx @@ -1,8 +1,7 @@ +import { useComponentsContext, useDictionary } from "@blocknote/react"; import { RiCheckFill } from "react-icons/ri"; -import { useComponentsContext } from "../../../editor/ComponentsContext"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; -import { useDictionary } from "../../../i18n/dictionary"; export const AcceptButton = () => { const dict = useDictionary(); diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx similarity index 84% rename from packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx rename to packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx index 071844aa7..f3a4a9909 100644 --- a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx @@ -1,9 +1,8 @@ -import { mockAIReplaceSelection } from "@blocknote/core"; +import { useComponentsContext, useDictionary } from "@blocknote/react"; import { RiLoopLeftFill } from "react-icons/ri"; -import { useComponentsContext } from "../../../editor/ComponentsContext"; +import { mockAIReplaceSelection } from "../../../../core/blocks/AIBlockContent/mockAIFunctions"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; -import { useDictionary } from "../../../i18n/dictionary"; import { AIInlineToolbarProps } from "../AIInlineToolbarProps"; export const RetryButton = ( diff --git a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx similarity index 89% rename from packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx rename to packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx index e023f20f0..e93b4d5f2 100644 --- a/packages/react/src/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx @@ -1,11 +1,10 @@ -import { TextSelection } from "@tiptap/pm/state"; +import { Block } from "@blocknote/core"; +import { useComponentsContext, useDictionary } from "@blocknote/react"; +import { TextSelection } from "prosemirror-state"; import { RiArrowGoBackFill } from "react-icons/ri"; -import { useComponentsContext } from "../../../editor/ComponentsContext"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; -import { useDictionary } from "../../../i18n/dictionary"; import { AIInlineToolbarProps } from "../AIInlineToolbarProps"; -import { Block } from "@blocknote/core"; export const RevertButton = ( props: AIInlineToolbarProps & { diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx b/packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx similarity index 94% rename from packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx rename to packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx index 87e6741f7..46d59ca87 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/AIButton.tsx +++ b/packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx @@ -1,10 +1,9 @@ import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { useComponentsContext, useDictionary } from "@blocknote/react"; import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react"; import { RiSparkling2Fill } from "react-icons/ri"; -import { useComponentsContext } from "../../../editor/ComponentsContext"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; -import { useDictionary } from "../../../i18n/dictionary"; export const AIButton = () => { const dict = useDictionary(); diff --git a/packages/ai/src/react/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/ai/src/react/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx new file mode 100644 index 000000000..dd832f0dd --- /dev/null +++ b/packages/ai/src/react/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -0,0 +1,44 @@ +import { + Block, + BlockSchema, + Dictionary, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { + BlockTypeSelect as CoreBlockTypeSelect, + blockTypeSelectItems as blockTypeSelectCoreItems, + useDictionary, +} from "@blocknote/react"; +import type { IconType } from "react-icons"; +import { RiSparkling2Fill } from "react-icons/ri"; + +export type BlockTypeSelectItem = { + name: string; + type: string; + props?: Record; + icon: IconType; + isSelected: ( + block: Block + ) => boolean; +}; + +export const blockTypeSelectItems = ( + dict: Dictionary +): BlockTypeSelectItem[] => [ + ...blockTypeSelectCoreItems(dict), + { + name: dict.slash_menu.ai_block.title, + type: "ai", + icon: RiSparkling2Fill, + isSelected: (block) => block.type === "ai", + }, +]; + +export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { + const dict = useDictionary(); + + return ( + + ); +}; diff --git a/packages/ai/src/react/components/FormattingToolbar/FormattingToolbar.tsx b/packages/ai/src/react/components/FormattingToolbar/FormattingToolbar.tsx new file mode 100644 index 000000000..b88c00d3e --- /dev/null +++ b/packages/ai/src/react/components/FormattingToolbar/FormattingToolbar.tsx @@ -0,0 +1,44 @@ +import { Dictionary } from "@blocknote/core"; +import { + FormattingToolbar as FormattingToolbarCore, + FormattingToolbarProps, + getFormattingToolbarItems as getFormattingToolbarCoreItems, +} from "@blocknote/react"; +import { ReactNode } from "react"; + +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; +import { AIButton } from "./DefaultButtons/AIButton"; +import { + BlockTypeSelectItem, + blockTypeSelectItems as defaultBlockTypeSelectItems, +} from "./DefaultSelects/BlockTypeSelect"; + +export const getFormattingToolbarItems = ( + dict: Dictionary, + blockTypeSelectItems?: BlockTypeSelectItem[] +): JSX.Element[] => [ + ...getFormattingToolbarCoreItems( + defaultBlockTypeSelectItems(dict) || blockTypeSelectItems + ), + , +]; + +export const FormattingToolbar = ( + props: FormattingToolbarProps & { children?: ReactNode } +) => { + const editor = useBlockNoteEditor(); + + return ( + + {props.children || + getFormattingToolbarItems( + editor.dictionary, + props.blockTypeSelectItems + )} + + ); +}; diff --git a/packages/ai/src/react/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/ai/src/react/components/FormattingToolbar/FormattingToolbarController.tsx new file mode 100644 index 000000000..71a09a4b0 --- /dev/null +++ b/packages/ai/src/react/components/FormattingToolbar/FormattingToolbarController.tsx @@ -0,0 +1,16 @@ +import { FC } from "react"; +import { + FormattingToolbarController as FormattingToolbarControllerCore, + FormattingToolbarProps, +} from "@blocknote/react"; +import { FormattingToolbar } from "./FormattingToolbar"; + +export const FormattingToolbarController = (props: { + formattingToolbar?: FC; +}) => { + return ( + + ); +}; diff --git a/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx b/packages/ai/src/react/components/SuggestionMenu/getDefaultAIMenuItems.tsx similarity index 71% rename from packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx rename to packages/ai/src/react/components/SuggestionMenu/getDefaultAIMenuItems.tsx index 2dd4de630..d463094b4 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultAIMenuItems.tsx +++ b/packages/ai/src/react/components/SuggestionMenu/getDefaultAIMenuItems.tsx @@ -1,10 +1,7 @@ -import { - BlockNoteEditor, - BlockSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { DefaultReactSuggestionItem } from "./types"; +import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { DefaultReactSuggestionItem } from "@blocknote/react"; + +import { BlockNoteEditor } from "../../../core/editor/BlockNoteEditor"; // TODO: Maybe we don't want to define the default AI prompts based on the // dictionary @@ -17,6 +14,7 @@ export function getDefaultAIMenuItems< query: string ): DefaultReactSuggestionItem[] { return Object.values(editor.dictionary.ai_menu).map((item) => ({ + dictKey: item.title as any, ...item, onItemClick: async () => { editor.aiInlineToolbar.open( diff --git a/packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx new file mode 100644 index 000000000..30c1b0f0d --- /dev/null +++ b/packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx @@ -0,0 +1,50 @@ +import { + BlockSchema, + InlineContentSchema, + insertOrUpdateBlock, + StyleSchema, +} from "@blocknote/core"; +import { RiSparkling2Fill } from "react-icons/ri"; +import { + DefaultReactSuggestionItem, + getDefaultReactSlashMenuItems as getDefaultCoreSlashMenuItems, +} from "@blocknote/react"; + +import { checkDefaultBlockTypeInSchema } from "../../../core/blocks/defaultBlockTypeGuards"; +import type { BlockNoteEditor } from "../../../core/editor/BlockNoteEditor"; + +const Icons = { + AI: RiSparkling2Fill, +}; + +export function getDefaultReactSlashMenuItems< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>(editor: BlockNoteEditor): DefaultReactSuggestionItem[] { + const items: DefaultReactSuggestionItem[] = + getDefaultCoreSlashMenuItems(editor); + const insertionIndex = items.findIndex((item) => item.dictKey === "emoji"); + + items.splice(insertionIndex, 0, { + dictKey: "ai", + onItemClick: () => editor.openSelectionMenu("`"), + ...editor.dictionary.slash_menu.ai, + icon: , + }); + + if (checkDefaultBlockTypeInSchema("ai", editor)) { + items.splice(insertionIndex, 0, { + dictKey: "ai_block", + onItemClick: () => { + insertOrUpdateBlock(editor, { + type: "ai", + }); + }, + ...editor.dictionary.slash_menu.ai_block, + icon: , + }); + } + + return items; +} diff --git a/packages/ai/src/react/editor/BlockNoteDefaultUI.tsx b/packages/ai/src/react/editor/BlockNoteDefaultUI.tsx new file mode 100644 index 000000000..4d86bb86b --- /dev/null +++ b/packages/ai/src/react/editor/BlockNoteDefaultUI.tsx @@ -0,0 +1,56 @@ +import { filterSuggestionItems } from "@blocknote/core"; +import { + BlockNoteDefaultUI as BlockNoteDefaultCoreUI, + BlockNoteDefaultUIProps as BlockNoteDefaultCoreUIProps, + SuggestionMenuController, +} from "@blocknote/react"; + +import { AIBlockToolbarController } from "../components/AIBlockToolbar/AIBlockToolbarController"; +import { AIInlineToolbarController } from "../components/AIInlineToolbar/AIInlineToolbarController"; +import { getDefaultAIMenuItems } from "../components/SuggestionMenu/getDefaultAIMenuItems"; +import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; +import { FormattingToolbarController } from "../components/FormattingToolbar/FormattingToolbarController"; +import { getDefaultReactSlashMenuItems } from "../components/SuggestionMenu/getDefaultReactSlashMenuItems"; + +export type BlockNoteDefaultUIProps = BlockNoteDefaultCoreUIProps & { + aiBlockToolbar?: boolean; + aiInlineToolbar?: boolean; + aiMenu?: boolean; +}; + +export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { + const editor = useBlockNoteEditor(); + + if (!editor) { + throw new Error( + "BlockNoteDefaultUI must be used within a BlockNoteContext.Provider" + ); + } + + return ( + <> + + {props.formattingToolbar !== false && } + {props.slashMenu !== false && ( + + filterSuggestionItems(getDefaultReactSlashMenuItems(editor), query) + } + /> + )} + {editor.aiBlockToolbar && props.aiBlockToolbar !== false && ( + + )} + {props.aiInlineToolbar !== false && } + {props.aiMenu !== false && ( + + filterSuggestionItems(getDefaultAIMenuItems(editor, query), query) + } + /> + )} + + ); +} diff --git a/packages/ai/src/react/editor/BlockNoteView.tsx b/packages/ai/src/react/editor/BlockNoteView.tsx new file mode 100644 index 000000000..fca74e393 --- /dev/null +++ b/packages/ai/src/react/editor/BlockNoteView.tsx @@ -0,0 +1,42 @@ +import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; +import { + BlockNoteViewComponent, + BlockNoteViewProps as BlockNoteViewCoreProps, + BlockNoteViewRaw as BlockNoteViewCoreRaw, +} from "@blocknote/react"; +import { + BlockNoteDefaultUI, + BlockNoteDefaultUIProps, +} from "./BlockNoteDefaultUI"; +import React, { ComponentProps } from "react"; + +export type BlockNoteViewProps< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema +> = BlockNoteViewCoreProps & BlockNoteDefaultUIProps; + +export const BlockNoteViewRaw = React.forwardRef((props, ref) => ( + + + +)) as < + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema +>( + props: ComponentProps< + typeof BlockNoteViewComponent + > & { + ref?: React.ForwardedRef; + } +) => ReturnType>; diff --git a/packages/ai/src/react/editor/style.css b/packages/ai/src/react/editor/style.css new file mode 100644 index 000000000..6f05f5bfe --- /dev/null +++ b/packages/ai/src/react/editor/style.css @@ -0,0 +1,3 @@ +.bn-side-menu[data-block-type="ai"]:not([data-prompt]) { + height: 58px; +} \ No newline at end of file diff --git a/packages/ai/src/react/hooks/useBlockNoteEditor.ts b/packages/ai/src/react/hooks/useBlockNoteEditor.ts new file mode 100644 index 000000000..be24f3824 --- /dev/null +++ b/packages/ai/src/react/hooks/useBlockNoteEditor.ts @@ -0,0 +1,21 @@ +import { + BlockNoteSchema, + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { useBlockNoteEditor as useBlockNoteEditorCore } from "@blocknote/react"; +import { BlockNoteEditor } from "../../core/editor/BlockNoteEditor"; + +export function useBlockNoteEditor< + BSchema extends BlockSchema = DefaultBlockSchema, + ISchema extends InlineContentSchema = DefaultInlineContentSchema, + SSchema extends StyleSchema = DefaultStyleSchema +>( + _schema?: BlockNoteSchema +): BlockNoteEditor { + return useBlockNoteEditorCore(_schema) as any; +} diff --git a/packages/ai/src/react/hooks/useCreateBlockNote.tsx b/packages/ai/src/react/hooks/useCreateBlockNote.tsx new file mode 100644 index 000000000..d80d0c255 --- /dev/null +++ b/packages/ai/src/react/hooks/useCreateBlockNote.tsx @@ -0,0 +1,34 @@ +import { + BlockNoteEditorOptions, + BlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { DependencyList, useMemo } from "react"; + +import { BlockNoteEditor, DefaultBlockSchema } from "../../core"; + +export const useCreateBlockNote = < + BSchema extends BlockSchema = DefaultBlockSchema, + ISchema extends InlineContentSchema = DefaultInlineContentSchema, + SSchema extends StyleSchema = DefaultStyleSchema +>( + options: Partial> = {}, + deps: DependencyList = [] +) => { + return useMemo(() => { + const editor = BlockNoteEditor.create(options); + if (window) { + // for testing / dev purposes + (window as any).ProseMirror = editor._tiptapEditor; + } + return editor; + }, deps); //eslint-disable-line react-hooks/exhaustive-deps +}; + +/** + * @deprecated use useCreateBlockNote instead + */ +export const useBlockNote = useCreateBlockNote; diff --git a/packages/ai/src/react/index.ts b/packages/ai/src/react/index.ts new file mode 100644 index 000000000..040fed92b --- /dev/null +++ b/packages/ai/src/react/index.ts @@ -0,0 +1,26 @@ +export * from "./components/AIBlockToolbar/DefaultButtons/ShowPromptButton"; +export * from "./components/AIBlockToolbar/DefaultButtons/UpdateButton"; +export * from "./components/AIBlockToolbar/AIBlockToolbar"; +export * from "./components/AIBlockToolbar/AIBlockToolbarController"; +export * from "./components/AIBlockToolbar/AIBlockToolbarProps"; + +export * from "./components/AIInlineToolbar/DefaultButtons/AcceptButton"; +export * from "./components/AIInlineToolbar/DefaultButtons/RetryButton"; +export * from "./components/AIInlineToolbar/DefaultButtons/RevertButton"; +export * from "./components/AIInlineToolbar/AIInlineToolbar"; +export * from "./components/AIInlineToolbar/AIInlineToolbarController"; +export * from "./components/AIInlineToolbar/AIInlineToolbarProps"; + +export * from "./components/FormattingToolbar/DefaultButtons/AIButton"; +export * from "./components/FormattingToolbar/DefaultSelects/BlockTypeSelect"; +export * from "./components/FormattingToolbar/FormattingToolbar"; +export * from "./components/FormattingToolbar/FormattingToolbarController"; + +export * from "./components/SuggestionMenu/getDefaultAIMenuItems"; +export * from "./components/SuggestionMenu/getDefaultReactSlashMenuItems"; + +export * from "./editor/BlockNoteDefaultUI"; +export * from "./editor/BlockNoteView"; + +export * from "./hooks/useBlockNoteEditor"; +export * from "./hooks/useCreateBlockNote"; diff --git a/packages/ai/src/style.css b/packages/ai/src/style.css new file mode 100644 index 000000000..c743f5981 --- /dev/null +++ b/packages/ai/src/style.css @@ -0,0 +1,3 @@ +@import url("@blocknote/react/style.css"); +@import url("/src/core/editor/style.css"); +@import url("/src/react/editor/style.css"); \ No newline at end of file diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json new file mode 100644 index 000000000..607ad93cf --- /dev/null +++ b/packages/ai/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "jsx": "react-jsx", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "dist", + "declaration": true, + "declarationDir": "types", + "composite": true, + "skipLibCheck": true, + }, + "include": ["src"], + "references": [ + { + "path": "../core" + }, + { + "path": "../react" + } + ] +} diff --git a/packages/ai/vite.config.ts b/packages/ai/vite.config.ts new file mode 100644 index 000000000..49c73d3d7 --- /dev/null +++ b/packages/ai/vite.config.ts @@ -0,0 +1,53 @@ +import react from "@vitejs/plugin-react"; +import * as path from "path"; +import { webpackStats } from "rollup-plugin-webpack-stats"; +import { defineConfig } from "vite"; +import pkg from "./package.json"; +// import eslintPlugin from "vite-plugin-eslint"; + +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + test: { + environment: "jsdom", + setupFiles: ["./vitestSetup.ts"], + }, + plugins: [react(), webpackStats()], + // used so that vitest resolves the core package from the sources instead of the built version + resolve: { + alias: + conf.command === "build" + ? ({} as Record) + : ({ + // load live from sources with live reload working + "@blocknote/core": path.resolve(__dirname, "../core/src/"), + "@blocknote/mantine": path.resolve(__dirname, "../mantine/src/"), + "@blocknote/react": path.resolve(__dirname, "../react/src/"), + } as Record), + }, + build: { + sourcemap: true, + lib: { + entry: path.resolve(__dirname, "src/index.ts"), + name: "blocknote-ai", + fileName: "blocknote-ai", + }, + rollupOptions: { + // make sure to externalize deps that shouldn't be bundled + // into your library + external: Object.keys({ + ...pkg.dependencies, + ...pkg.peerDependencies, + ...pkg.devDependencies, + }), + output: { + // Provide global variables to use in the UMD build + // for externalized deps + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + interop: "compat", // https://rollupjs.org/migration/#changed-defaults + }, + }, + }, +})); diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 748ea3957..e2b9b9e8d 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -30,7 +30,6 @@ 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, @@ -43,7 +42,6 @@ export const defaultBlockSpecs = { image: ImageBlock, video: VideoBlock, audio: AudioBlock, - ai: AIBlock, } satisfies BlockSpecs; export const defaultBlockSchema = getBlockSchemaFromSpecs(defaultBlockSpecs); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index c44a18c86..c9bec807b 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -26,8 +26,6 @@ import { DefaultStyleSchema, PartialBlock, } from "../blocks/defaultBlocks"; -import { AIBlockToolbarProsemirrorPlugin } from "../extensions/AIBlockToolbar/AIBlockToolbarPlugin"; -import { AIInlineToolbarProsemirrorPlugin } from "../extensions/AIInlineToolbar/AIInlineToolbarPlugin"; import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin"; import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin"; import { LinkToolbarProsemirrorPlugin } from "../extensions/LinkToolbar/LinkToolbarPlugin"; @@ -245,8 +243,6 @@ export class BlockNoteEditor< ISchema, SSchema >; - public readonly aiBlockToolbar?: AIBlockToolbarProsemirrorPlugin; - public readonly aiInlineToolbar: AIInlineToolbarProsemirrorPlugin; /** * The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload). @@ -275,8 +271,8 @@ export class BlockNoteEditor< return new BlockNoteEditor(options); } - private constructor( - private readonly options: Partial> + protected constructor( + protected readonly options: Partial> ) { const anyOpts = options as any; if (anyOpts.onEditorContentChange) { @@ -332,11 +328,6 @@ export class BlockNoteEditor< if (checkDefaultBlockTypeInSchema("table", this)) { this.tableHandles = new TableHandlesProsemirrorPlugin(this as any); } - if (checkDefaultBlockTypeInSchema("ai", this)) { - this.aiBlockToolbar = new AIBlockToolbarProsemirrorPlugin(); - } - - this.aiInlineToolbar = new AIInlineToolbarProsemirrorPlugin(); const extensions = getBlockNoteExtensions({ editor: this, @@ -360,8 +351,6 @@ export class BlockNoteEditor< this.suggestionMenus.plugin, ...(this.filePanel ? [this.filePanel.plugin] : []), ...(this.tableHandles ? [this.tableHandles.plugin] : []), - ...(this.aiBlockToolbar ? [this.aiBlockToolbar.plugin] : []), - this.aiInlineToolbar.plugin, PlaceholderPlugin(this, newOptions.placeholders), ]; }, diff --git a/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts b/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts index 874c7e318..4387ac768 100644 --- a/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +++ b/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts @@ -1,7 +1,7 @@ import type { Dictionary } from "../../i18n/dictionary"; export type DefaultSuggestionItem = { - key: keyof Dictionary["slash_menu"]; + dictKey: keyof Dictionary["slash_menu"]; title: string; onItemClick: () => void; subtext?: string; diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 2347a14bb..58362ccf7 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -91,7 +91,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-1"), - key: "heading", + dictKey: "heading", ...editor.dictionary.slash_menu.heading, }, { @@ -102,7 +102,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-2"), - key: "heading_2", + dictKey: "heading_2", ...editor.dictionary.slash_menu.heading_2, }, { @@ -113,7 +113,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-3"), - key: "heading_3", + dictKey: "heading_3", ...editor.dictionary.slash_menu.heading_3, } ); @@ -127,7 +127,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Shift-7"), - key: "numbered_list", + dictKey: "numbered_list", ...editor.dictionary.slash_menu.numbered_list, }); } @@ -140,7 +140,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Shift-8"), - key: "bullet_list", + dictKey: "bullet_list", ...editor.dictionary.slash_menu.bullet_list, }); } @@ -153,7 +153,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Shift-9"), - key: "check_list", + dictKey: "check_list", ...editor.dictionary.slash_menu.check_list, }); } @@ -166,7 +166,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-0"), - key: "paragraph", + dictKey: "paragraph", ...editor.dictionary.slash_menu.paragraph, }); } @@ -190,7 +190,7 @@ export function getDefaultSlashMenuItems< }); }, badge: undefined, - key: "table", + dictKey: "table", ...editor.dictionary.slash_menu.table, }); } @@ -209,7 +209,7 @@ export function getDefaultSlashMenuItems< }) ); }, - key: "image", + dictKey: "image", ...editor.dictionary.slash_menu.image, }); } @@ -228,7 +228,7 @@ export function getDefaultSlashMenuItems< }) ); }, - key: "video", + dictKey: "video", ...editor.dictionary.slash_menu.video, }); } @@ -247,7 +247,7 @@ export function getDefaultSlashMenuItems< }) ); }, - key: "audio", + dictKey: "audio", ...editor.dictionary.slash_menu.audio, }); } @@ -266,32 +266,14 @@ export function getDefaultSlashMenuItems< }) ); }, - key: "image", + dictKey: "image", ...editor.dictionary.slash_menu.file, }); } - if (checkDefaultBlockTypeInSchema("ai", editor)) { - items.push({ - onItemClick: () => { - insertOrUpdateBlock(editor, { - type: "ai", - }); - }, - key: "ai_block", - ...editor.dictionary.slash_menu.ai_block, - }); - } - - items.push({ - onItemClick: () => editor.openSelectionMenu("`"), - key: "ai", - ...editor.dictionary.slash_menu.ai, - }); - items.push({ onItemClick: () => editor.openSelectionMenu(":"), - key: "emoji", + dictKey: "emoji", ...editor.dictionary.slash_menu.emoji, }); diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index b2a75db92..6750b6a2b 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -318,6 +318,7 @@ export const ar: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index cb26d5ae8..dd4723f3c 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -331,7 +331,8 @@ export const en = { }, }, ai_block_toolbar: { - show_prompt: "Show prompt", + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index 1dd7e1512..020168d54 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -333,6 +333,7 @@ export const fr: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index 6837eb094..5bd294bb1 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -325,6 +325,7 @@ export const is: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index 84c620d65..ca11d553f 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -353,6 +353,7 @@ export const ja: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 0e9088fad..83f86299a 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -346,6 +346,7 @@ export const ko: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index c6df47164..b03445cb3 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -332,6 +332,7 @@ export const nl: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index 9d74ba323..b03397692 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -317,6 +317,7 @@ export const pl: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 3a4e69718..61d25328c 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -325,6 +325,7 @@ export const pt: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index 31e69f770..6a3ba7197 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -360,6 +360,7 @@ export const ru: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index a05dc9668..098f8832d 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -332,6 +332,7 @@ export const vi: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index ba83ab06f..9d66ec8a1 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -366,6 +366,7 @@ export const zh: Dictionary = { }, ai_block_toolbar: { show_prompt: "Show prompt", + show_prompt_datetime_tooltip: "Generated:", update: "Update", updating: "Updating…", }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7c5903e0e..b02f28a77 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,9 +1,8 @@ import * as locales from "./i18n/locales"; +export * from "./api/getBlockInfoFromPos"; export * from "./api/exporters/html/externalHTMLExporter"; export * from "./api/exporters/html/internalHTMLSerializer"; export * from "./api/testUtil"; -export * from "./blocks/AIBlockContent/AIBlockContent"; -export * from "./blocks/AIBlockContent/mockAIFunctions"; export * from "./blocks/AudioBlockContent/AudioBlockContent"; export * from "./blocks/FileBlockContent/FileBlockContent"; export * from "./blocks/ImageBlockContent/ImageBlockContent"; @@ -33,6 +32,7 @@ export * from "./extensions/TableHandles/TableHandlesPlugin"; export * from "./i18n/dictionary"; export * from "./schema"; export * from "./util/browser"; +export * from "./util/EventEmitter"; export * from "./util/string"; export * from "./util/typescript"; export { UnreachableCaseError, assertEmpty } from "./util/typescript"; diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index 3c0c2c14a..3c708893a 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -12,7 +12,7 @@ import { usePrefersColorScheme, } from "@blocknote/react"; import { MantineProvider } from "@mantine/core"; -import { ComponentProps, useCallback } from "react"; +import { ComponentProps, FC, useCallback } from "react"; import { Theme, @@ -142,6 +142,9 @@ export const BlockNoteView = < light: Theme; dark: Theme; }; + viewComponent?: FC< + ComponentProps> + >; } ) => { const { className, theme, ...rest } = props; @@ -176,6 +179,8 @@ export const BlockNoteView = < [defaultColorScheme, theme] ); + const ViewComponent = props.viewComponent || BlockNoteViewRaw; + return ( {/* `cssVariablesSelector` scopes Mantine CSS variables to only the editor, */} @@ -187,7 +192,7 @@ export const BlockNoteView = < // don't need this attribute (we use our own theming API), we return // undefined here. getRootElement={() => undefined}> - ( } }} onClick={onClick} + leftSection={icon} aria-pressed={isSelected} data-selected={isSelected || undefined} data-test={ diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index 457eca2b1..7b02f5279 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -14,7 +14,6 @@ import { RiListCheck3, RiListOrdered, RiListUnordered, - RiSparkling2Fill, RiText, } from "react-icons/ri"; @@ -94,12 +93,6 @@ export const blockTypeSelectItems = ( icon: RiListCheck3, isSelected: (block) => block.type === "checkListItem", }, - { - name: dict.slash_menu.ai_block.title, - type: "ai", - icon: RiSparkling2Fill, - isSelected: (block) => block.type === "ai", - }, ]; export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx index 23bb3276b..ba591a608 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbar.tsx @@ -20,7 +20,6 @@ import { FileRenameButton } from "./DefaultButtons/FileRenameButton"; import { FileDownloadButton } from "./DefaultButtons/FileDownloadButton"; import { FilePreviewButton } from "./DefaultButtons/FilePreviewButton"; import { FileDeleteButton } from "./DefaultButtons/FileDeleteButton"; -import { AIButton } from "./DefaultButtons/AIButton"; export const getFormattingToolbarItems = ( blockTypeSelectItems?: BlockTypeSelectItem[] @@ -46,7 +45,6 @@ export const getFormattingToolbarItems = ( , , , - , ]; // TODO: props.blockTypeSelectItems should only be available if no children diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx index 9dde41c4f..5fbc91646 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx @@ -16,11 +16,11 @@ import { RiListCheck3, RiListOrdered, RiListUnordered, - RiSparkling2Fill, RiTable2, RiText, RiVolumeUpFill, } from "react-icons/ri"; + import { DefaultReactSuggestionItem } from "./types"; const icons = { @@ -36,8 +36,6 @@ const icons = { video: RiFilmLine, audio: RiVolumeUpFill, file: RiFile2Line, - ai_block: RiSparkling2Fill, - ai: RiSparkling2Fill, emoji: RiEmotionFill, }; @@ -47,7 +45,7 @@ export function getDefaultReactSlashMenuItems< S extends StyleSchema >(editor: BlockNoteEditor): DefaultReactSuggestionItem[] { return getDefaultSlashMenuItems(editor).map((item) => { - const Icon = icons[item.key]; + const Icon = icons[item.dictKey as keyof typeof icons]; // TODO return { ...item, icon: , diff --git a/packages/react/src/components/SuggestionMenu/types.tsx b/packages/react/src/components/SuggestionMenu/types.tsx index a3d796833..5c0753a1a 100644 --- a/packages/react/src/components/SuggestionMenu/types.tsx +++ b/packages/react/src/components/SuggestionMenu/types.tsx @@ -4,7 +4,7 @@ import { DefaultSuggestionItem } from "@blocknote/core"; * Although any arbitrary data can be passed as suggestion items, the built-in * UI components such as `MantineSuggestionMenu` expect a shape that conforms to DefaultSuggestionItem */ -export type DefaultReactSuggestionItem = Omit & { +export type DefaultReactSuggestionItem = DefaultSuggestionItem & { icon?: JSX.Element; }; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 3f101ee5b..237c843bb 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -6,10 +6,6 @@ import { SuggestionMenuController } from "../components/SuggestionMenu/Suggestio import { GridSuggestionMenuController } from "../components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; -import { AIBlockToolbarController } from "../components/AIBlockToolbar/AIBlockToolbarController"; -import { AIInlineToolbarController } from "../components/AIInlineToolbar/AIInlineToolbarController"; -import { filterSuggestionItems } from "@blocknote/core"; -import { getDefaultAIMenuItems } from "../components/SuggestionMenu/getDefaultAIMenuItems"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -19,9 +15,6 @@ export type BlockNoteDefaultUIProps = { filePanel?: boolean; tableHandles?: boolean; emojiPicker?: boolean; - aiBlockToolbar?: boolean; - aiInlineToolbar?: boolean; - aiMenu?: boolean; }; export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { @@ -52,18 +45,6 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { {editor.tableHandles && props.tableHandles !== false && ( )} - {editor.aiBlockToolbar && props.aiBlockToolbar !== false && ( - - )} - {props.aiInlineToolbar !== false && } - {props.aiMenu !== false && ( - - filterSuggestionItems(getDefaultAIMenuItems(editor, query), query) - } - /> - )} ); } diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index 9294cb5b8..443c8dba6 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -64,7 +64,7 @@ export type BlockNoteViewProps< > & BlockNoteDefaultUIProps; -function BlockNoteViewComponent< +export function BlockNoteViewComponent< BSchema extends BlockSchema, ISchema extends InlineContentSchema, SSchema extends StyleSchema diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css index 2fece0e28..0d4376824 100644 --- a/packages/react/src/editor/styles.css +++ b/packages/react/src/editor/styles.css @@ -237,8 +237,4 @@ .bn-side-menu[data-url="false"] { height: 54px; -} - -.bn-side-menu[data-block-type="ai"]:not([data-prompt]) { - height: 58px; -} +} \ No newline at end of file diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 10e265911..364969f48 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -82,6 +82,8 @@ export * from "./hooks/useEditorForceUpdate"; export * from "./hooks/useEditorSelectionChange"; export * from "./hooks/usePrefersColorScheme"; export * from "./hooks/useSelectedBlocks"; +export * from "./hooks/useUIElementPositioning"; +export * from "./hooks/useUIPluginState"; export * from "./schema/ReactBlockSpec"; export * from "./schema/ReactInlineContentSpec"; diff --git a/playground/package.json b/playground/package.json index 2b9e46b37..b8fe69a73 100644 --- a/playground/package.json +++ b/playground/package.json @@ -13,6 +13,7 @@ "@aws-sdk/client-s3": "^3.609.0", "@aws-sdk/s3-request-presigner": "^3.609.0", "@blocknote/ariakit": "^0.15.3", + "@blocknote/ai": "^0.15.3", "@blocknote/core": "^0.15.3", "@blocknote/mantine": "^0.15.3", "@blocknote/react": "^0.15.3", diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 17fba07d2..3b4fb3fe9 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -21,6 +21,7 @@ "include": ["src", "../examples"], "references": [ { "path": "./tsconfig.node.json" }, + { "path": "../packages/ai/" }, { "path": "../packages/core/" }, { "path": "../packages/react/" }, { "path": "../packages/shadcn/" } diff --git a/playground/vite.config.ts b/playground/vite.config.ts index ead9639a7..7c2ccedea 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -33,6 +33,7 @@ export default defineConfig((conf) => ({ : { // Comment out the lines below to load a built version of blocknote // or, keep as is to load live from sources with live reload working + "@blocknote/ai": path.resolve(__dirname, "../packages/ai/src/"), "@blocknote/core": path.resolve(__dirname, "../packages/core/src/"), "@blocknote/react": path.resolve( __dirname, From 22db2b4d5ff2a9287b1d9eb806602abf9bd61776 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 10 Sep 2024 00:56:38 +0200 Subject: [PATCH 06/45] Finished initial package split --- examples/01-basic/01-minimal/App.tsx | 19 ++++-- .../BlueButton.tsx | 4 +- .../App.tsx | 1 + .../11-uppy-file-panel/FileReplaceButton.tsx | 2 +- .../link-toolbar-buttons/AlertButton.tsx | 4 +- .../06-custom-schema/01-alert-block/App.tsx | 1 + .../02-suggestion-menus-mentions/App.tsx | 1 + .../06-custom-schema/03-font-style/App.tsx | 2 +- .../04-pdf-file-block/App.tsx | 1 + .../react-custom-styles/App.tsx | 8 +-- .../react-vanilla-custom-styles/App.tsx | 8 +-- .../ai/src/core/editor/BlockNoteEditor.ts | 18 +++++- .../getDefaultSlashMenuItems.ts | 6 +- packages/ai/src/core/i18n/dictionary.ts | 17 ++++++ packages/ai/src/core/i18n/locales/ar.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/en.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/fr.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/index.ts | 12 ++++ packages/ai/src/core/i18n/locales/is.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/ja.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/ko.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/nl.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/pl.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/pt.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/ru.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/vi.ts | 54 +++++++++++++++++ packages/ai/src/core/i18n/locales/zh.ts | 54 +++++++++++++++++ packages/ai/src/index.ts | 1 - packages/ai/src/mantine/index.tsx | 28 --------- .../AIBlockToolbar/AIBlockToolbar.tsx | 4 +- .../DefaultButtons/ShowPromptButton.tsx | 7 ++- .../DefaultButtons/UpdateButton.tsx | 7 ++- .../AIInlineToolbar/AIInlineToolbar.tsx | 4 +- .../DefaultButtons/AcceptButton.tsx | 5 +- .../DefaultButtons/RetryButton.tsx | 7 ++- .../DefaultButtons/RevertButton.tsx | 5 +- .../DefaultButtons/AIButton.tsx | 5 +- .../DefaultSelects/BlockTypeSelect.tsx | 5 +- .../FormattingToolbar/FormattingToolbar.tsx | 2 +- .../SuggestionMenu/getDefaultAIMenuItems.tsx | 2 +- .../getDefaultReactSlashMenuItems.tsx | 6 +- .../ai/src/react/editor/BlockNoteView.tsx | 42 ------------- .../ai/src/react/hooks/useBlockNoteEditor.ts | 6 +- .../ai/src/react/hooks/useCreateBlockNote.tsx | 7 ++- packages/ai/src/react/hooks/useDictionary.ts | 7 +++ packages/ai/src/react/index.ts | 1 - packages/ariakit/src/index.tsx | 14 +---- packages/ariakit/src/toolbar/Toolbar.tsx | 3 +- .../ariakit/src/toolbar/ToolbarButton.tsx | 3 +- .../ariakit/src/toolbar/ToolbarSelect.tsx | 2 +- .../SuggestionMenu/DefaultSuggestionItem.ts | 4 +- .../getDefaultSlashMenuItems.ts | 26 ++++---- packages/core/src/i18n/locales/ar.ts | 40 ------------- packages/core/src/i18n/locales/en.ts | 40 ------------- packages/core/src/i18n/locales/fr.ts | 40 ------------- packages/core/src/i18n/locales/index.ts | 2 +- packages/core/src/i18n/locales/is.ts | 40 ------------- packages/core/src/i18n/locales/ja.ts | 40 ------------- packages/core/src/i18n/locales/ko.ts | 40 ------------- packages/core/src/i18n/locales/nl.ts | 40 ------------- packages/core/src/i18n/locales/pl.ts | 40 ------------- packages/core/src/i18n/locales/pt.ts | 40 ------------- packages/core/src/i18n/locales/ru.ts | 40 ------------- packages/core/src/i18n/locales/vi.ts | 40 ------------- packages/core/src/i18n/locales/zh.ts | 40 ------------- packages/core/src/index.ts | 1 + packages/mantine/src/index.tsx | 14 +---- packages/mantine/src/toolbar/Toolbar.tsx | 3 +- .../mantine/src/toolbar/ToolbarButton.tsx | 3 +- .../mantine/src/toolbar/ToolbarSelect.tsx | 2 +- .../DefaultButtons/BasicTextStyleButton.tsx | 2 +- .../DefaultButtons/ColorStyleButton.tsx | 2 +- .../DefaultButtons/CreateLinkButton.tsx | 2 +- .../DefaultButtons/FileCaptionButton.tsx | 2 +- .../DefaultButtons/FileDeleteButton.tsx | 2 +- .../DefaultButtons/FileDownloadButton.tsx | 2 +- .../DefaultButtons/FilePreviewButton.tsx | 2 +- .../DefaultButtons/FileRenameButton.tsx | 2 +- .../DefaultButtons/FileReplaceButton.tsx | 2 +- .../DefaultButtons/NestBlockButtons.tsx | 4 +- .../DefaultButtons/TextAlignButton.tsx | 2 +- .../DefaultSelects/BlockTypeSelect.tsx | 7 +-- .../FormattingToolbar/FormattingToolbar.tsx | 5 +- .../DefaultButtons/DeleteLinkButton.tsx | 2 +- .../DefaultButtons/EditLinkButton.tsx | 4 +- .../DefaultButtons/OpenLinkButton.tsx | 2 +- .../components/LinkToolbar/LinkToolbar.tsx | 8 +-- .../SuggestionMenu/SuggestionMenu.test.tsx | 1 + .../getDefaultReactSlashMenuItems.tsx | 5 +- .../react/src/editor/ComponentsContext.tsx | 60 +------------------ packages/shadcn/src/index.tsx | 14 +---- packages/shadcn/src/toolbar/Toolbar.tsx | 8 +-- tests/src/utils/customblocks/Alert.tsx | 1 + 93 files changed, 840 insertions(+), 761 deletions(-) create mode 100644 packages/ai/src/core/i18n/dictionary.ts create mode 100644 packages/ai/src/core/i18n/locales/ar.ts create mode 100644 packages/ai/src/core/i18n/locales/en.ts create mode 100644 packages/ai/src/core/i18n/locales/fr.ts create mode 100644 packages/ai/src/core/i18n/locales/index.ts create mode 100644 packages/ai/src/core/i18n/locales/is.ts create mode 100644 packages/ai/src/core/i18n/locales/ja.ts create mode 100644 packages/ai/src/core/i18n/locales/ko.ts create mode 100644 packages/ai/src/core/i18n/locales/nl.ts create mode 100644 packages/ai/src/core/i18n/locales/pl.ts create mode 100644 packages/ai/src/core/i18n/locales/pt.ts create mode 100644 packages/ai/src/core/i18n/locales/ru.ts create mode 100644 packages/ai/src/core/i18n/locales/vi.ts create mode 100644 packages/ai/src/core/i18n/locales/zh.ts delete mode 100644 packages/ai/src/mantine/index.tsx delete mode 100644 packages/ai/src/react/editor/BlockNoteView.tsx create mode 100644 packages/ai/src/react/hooks/useDictionary.ts diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index a302d17a8..6b52205fa 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -1,13 +1,24 @@ import "@blocknote/core/fonts/inter.css"; -import { useCreateBlockNote, BlockNoteView } from "@blocknote/ai"; +import { useCreateBlockNote, BlockNoteDefaultUI } from "@blocknote/ai"; import "@blocknote/mantine/style.css"; -// import { useCreateBlockNote } from "@blocknote/react"; -// import { BlockNoteView } from "@blocknote/mantine"; +import { BlockNoteView } from "@blocknote/mantine"; export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote(); // Renders the editor instance using a React component. - return ; + return ( + + + + ); } diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/BlueButton.tsx b/examples/03-ui-components/02-formatting-toolbar-buttons/BlueButton.tsx index 48400a273..ebbf6a580 100644 --- a/examples/03-ui-components/02-formatting-toolbar-buttons/BlueButton.tsx +++ b/examples/03-ui-components/02-formatting-toolbar-buttons/BlueButton.tsx @@ -27,7 +27,7 @@ export function BlueButton() { }, editor); return ( - { editor.toggleStyles({ @@ -37,6 +37,6 @@ export function BlueButton() { }} isSelected={isSelected}> Blue - + ); } diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/App.tsx b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/App.tsx index 984cf3d4e..a0043c6cb 100644 --- a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/App.tsx +++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/App.tsx @@ -16,6 +16,7 @@ import { HiOutlineGlobeAlt } from "react-icons/hi"; // Custom Slash Menu item to insert a block after the current one. const insertHelloWorldItem = (editor: BlockNoteEditor) => ({ + name: "hello_world", title: "Insert Hello World", onItemClick: () => { // Block that the text cursor is currently in. diff --git a/examples/03-ui-components/11-uppy-file-panel/FileReplaceButton.tsx b/examples/03-ui-components/11-uppy-file-panel/FileReplaceButton.tsx index 0761150d1..ad06a10cd 100644 --- a/examples/03-ui-components/11-uppy-file-panel/FileReplaceButton.tsx +++ b/examples/03-ui-components/11-uppy-file-panel/FileReplaceButton.tsx @@ -48,7 +48,7 @@ export const FileReplaceButton = () => { return ( - setIsOpen(!isOpen)} isSelected={isOpen} diff --git a/examples/03-ui-components/link-toolbar-buttons/AlertButton.tsx b/examples/03-ui-components/link-toolbar-buttons/AlertButton.tsx index b393d6077..6e322fca0 100644 --- a/examples/03-ui-components/link-toolbar-buttons/AlertButton.tsx +++ b/examples/03-ui-components/link-toolbar-buttons/AlertButton.tsx @@ -5,12 +5,12 @@ export function AlertButton(props: LinkToolbarProps) { const Components = useComponentsContext()!; return ( - { window.alert(`Link URL: ${props.url}`); }}> Open Alert - + ); } diff --git a/examples/06-custom-schema/01-alert-block/App.tsx b/examples/06-custom-schema/01-alert-block/App.tsx index dcaefdfbd..36ac733ac 100644 --- a/examples/06-custom-schema/01-alert-block/App.tsx +++ b/examples/06-custom-schema/01-alert-block/App.tsx @@ -29,6 +29,7 @@ const schema = BlockNoteSchema.create({ // Slash menu item to insert an Alert block const insertAlert = (editor: typeof schema.BlockNoteEditor) => ({ + name: "alert", title: "Alert", onItemClick: () => { insertOrUpdateBlock(editor, { diff --git a/examples/06-custom-schema/02-suggestion-menus-mentions/App.tsx b/examples/06-custom-schema/02-suggestion-menus-mentions/App.tsx index e86e4594e..42aa46653 100644 --- a/examples/06-custom-schema/02-suggestion-menus-mentions/App.tsx +++ b/examples/06-custom-schema/02-suggestion-menus-mentions/App.tsx @@ -32,6 +32,7 @@ const getMentionMenuItems = ( const users = ["Steve", "Bob", "Joe", "Mike"]; return users.map((user) => ({ + name: user.toLowerCase(), title: user, onItemClick: () => { editor.insertInlineContent([ diff --git a/examples/06-custom-schema/03-font-style/App.tsx b/examples/06-custom-schema/03-font-style/App.tsx index 7c881fbf1..6d0da4e5d 100644 --- a/examples/06-custom-schema/03-font-style/App.tsx +++ b/examples/06-custom-schema/03-font-style/App.tsx @@ -44,7 +44,7 @@ const SetFontStyleButton = () => { const Components = useComponentsContext()!; return ( - } diff --git a/examples/06-custom-schema/04-pdf-file-block/App.tsx b/examples/06-custom-schema/04-pdf-file-block/App.tsx index 077b33152..fc3a6d529 100644 --- a/examples/06-custom-schema/04-pdf-file-block/App.tsx +++ b/examples/06-custom-schema/04-pdf-file-block/App.tsx @@ -30,6 +30,7 @@ const schema = BlockNoteSchema.create({ // Slash menu item to insert a PDF block const insertPDF = (editor: typeof schema.BlockNoteEditor) => ({ + name: "pdf", title: "PDF", onItemClick: () => { insertOrUpdateBlock(editor, { diff --git a/examples/06-custom-schema/react-custom-styles/App.tsx b/examples/06-custom-schema/react-custom-styles/App.tsx index f19533c79..107b86a7e 100644 --- a/examples/06-custom-schema/react-custom-styles/App.tsx +++ b/examples/06-custom-schema/react-custom-styles/App.tsx @@ -55,7 +55,7 @@ const CustomFormattingToolbar = (props: FormattingToolbarProps) => { return ( - { editor.toggleStyles({ @@ -64,8 +64,8 @@ const CustomFormattingToolbar = (props: FormattingToolbarProps) => { }} isSelected={activeStyles.small}> Small - - + { editor.toggleStyles({ @@ -74,7 +74,7 @@ const CustomFormattingToolbar = (props: FormattingToolbarProps) => { }} isSelected={!!activeStyles.fontSize}> Font size - + ); }; diff --git a/examples/vanilla-js/react-vanilla-custom-styles/App.tsx b/examples/vanilla-js/react-vanilla-custom-styles/App.tsx index 813952cda..115a5b556 100644 --- a/examples/vanilla-js/react-vanilla-custom-styles/App.tsx +++ b/examples/vanilla-js/react-vanilla-custom-styles/App.tsx @@ -67,7 +67,7 @@ const CustomFormattingToolbar = (props: FormattingToolbarProps) => { return ( - { editor.toggleStyles({ @@ -76,8 +76,8 @@ const CustomFormattingToolbar = (props: FormattingToolbarProps) => { }} isSelected={activeStyles.small}> Small - - + { editor.toggleStyles({ @@ -86,7 +86,7 @@ const CustomFormattingToolbar = (props: FormattingToolbarProps) => { }} isSelected={!!activeStyles.fontSize}> Font size - + ); }; diff --git a/packages/ai/src/core/editor/BlockNoteEditor.ts b/packages/ai/src/core/editor/BlockNoteEditor.ts index f8b7aa225..53f48570e 100644 --- a/packages/ai/src/core/editor/BlockNoteEditor.ts +++ b/packages/ai/src/core/editor/BlockNoteEditor.ts @@ -1,6 +1,6 @@ import { BlockNoteEditor as BlockNoteCoreEditor, - BlockNoteEditorOptions, + BlockNoteEditorOptions as BlockNoteCoreEditorOptions, BlockNoteSchema, BlockSchema, DefaultInlineContentSchema, @@ -13,13 +13,28 @@ import { Extension } from "@tiptap/core"; import { DefaultBlockSchema, defaultBlockSpecs } from "../blocks/defaultBlocks"; import { AIBlockToolbarProsemirrorPlugin } from "../extensions/AIBlockToolbar/AIBlockToolbarPlugin"; import { AIInlineToolbarProsemirrorPlugin } from "../extensions/AIInlineToolbar/AIInlineToolbarPlugin"; +import { Dictionary } from "../i18n/dictionary"; +import { en } from "../i18n/locales"; // import { checkDefaultBlockTypeInSchema } from "../blocks/defaultBlockTypeGuards"; +export type BlockNoteEditorOptions< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema +> = Omit< + BlockNoteCoreEditorOptions, + "dictionary" +> & { + dictionary?: Dictionary; +}; + export class BlockNoteEditor< BSchema extends BlockSchema = DefaultBlockSchema, ISchema extends InlineContentSchema = DefaultInlineContentSchema, SSchema extends StyleSchema = DefaultStyleSchema > extends BlockNoteCoreEditor { + public declare dictionary: Dictionary; + public readonly aiBlockToolbar?: AIBlockToolbarProsemirrorPlugin = new AIBlockToolbarProsemirrorPlugin(); public readonly aiInlineToolbar: AIInlineToolbarProsemirrorPlugin = @@ -33,6 +48,7 @@ export class BlockNoteEditor< blockSpecs: defaultBlockSpecs, }), ...options, + dictionary: options.dictionary || en, _tiptapOptions: { extensions: [ Extension.create({ diff --git a/packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 08b10c2fd..438812fc9 100644 --- a/packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/ai/src/core/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -16,11 +16,11 @@ export function getDefaultSlashMenuItems< S extends StyleSchema >(editor: BlockNoteEditor) { const items: DefaultSuggestionItem[] = getDefaultCoreSlashMenuItems(editor); - const insertionIndex = items.findIndex((item) => item.key === "emoji"); + const insertionIndex = items.findIndex((item) => item.name === "emoji"); items.splice(insertionIndex, 0, { onItemClick: () => editor.openSelectionMenu("`"), - key: "ai", + name: "ai", ...editor.dictionary.slash_menu.ai, }); @@ -31,7 +31,7 @@ export function getDefaultSlashMenuItems< type: "ai", }); }, - key: "ai_block", + name: "ai_block", ...editor.dictionary.slash_menu.ai_block, }); } diff --git a/packages/ai/src/core/i18n/dictionary.ts b/packages/ai/src/core/i18n/dictionary.ts new file mode 100644 index 000000000..c93e6533c --- /dev/null +++ b/packages/ai/src/core/i18n/dictionary.ts @@ -0,0 +1,17 @@ +// function scramble(dict: any) { +// const newDict: any = {} as any; + +import type { en } from "./locales"; + +// for (const key in dict) { +// if (typeof dict[key] === "object") { +// newDict[key] = scramble(dict[key]); +// } else { +// newDict[key] = dict[key].split("").reverse().join(""); +// } +// } + +// return newDict; +// } + +export type Dictionary = typeof en; diff --git a/packages/ai/src/core/i18n/locales/ar.ts b/packages/ai/src/core/i18n/locales/ar.ts new file mode 100644 index 000000000..3b66003d8 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/ar.ts @@ -0,0 +1,54 @@ +import { ar as dict } from "@blocknote/core"; + +export const ar = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/en.ts b/packages/ai/src/core/i18n/locales/en.ts new file mode 100644 index 000000000..9b84b2801 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/en.ts @@ -0,0 +1,54 @@ +import { en as dict } from "@blocknote/core"; + +export const en = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/fr.ts b/packages/ai/src/core/i18n/locales/fr.ts new file mode 100644 index 000000000..5f7d4f46e --- /dev/null +++ b/packages/ai/src/core/i18n/locales/fr.ts @@ -0,0 +1,54 @@ +import { fr as dict } from "@blocknote/core"; + +export const fr = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/index.ts b/packages/ai/src/core/i18n/locales/index.ts new file mode 100644 index 000000000..eb39fd008 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/index.ts @@ -0,0 +1,12 @@ +export * from "./ar"; +export * from "./en"; +export * from "./fr"; +export * from "./is"; +export * from "./ja"; +export * from "./ko"; +export * from "./nl"; +export * from "./pl"; +export * from "./pt"; +export * from "./vi"; +export * from "./zh"; +export * from "./ru"; diff --git a/packages/ai/src/core/i18n/locales/is.ts b/packages/ai/src/core/i18n/locales/is.ts new file mode 100644 index 000000000..55ed4a9cc --- /dev/null +++ b/packages/ai/src/core/i18n/locales/is.ts @@ -0,0 +1,54 @@ +import { is as dict } from "@blocknote/core"; + +export const is = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/ja.ts b/packages/ai/src/core/i18n/locales/ja.ts new file mode 100644 index 000000000..47d4e4396 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/ja.ts @@ -0,0 +1,54 @@ +import { ja as dict } from "@blocknote/core"; + +export const ja = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/ko.ts b/packages/ai/src/core/i18n/locales/ko.ts new file mode 100644 index 000000000..b8fe73a6d --- /dev/null +++ b/packages/ai/src/core/i18n/locales/ko.ts @@ -0,0 +1,54 @@ +import { ko as dict } from "@blocknote/core"; + +export const ko = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/nl.ts b/packages/ai/src/core/i18n/locales/nl.ts new file mode 100644 index 000000000..66f6958c2 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/nl.ts @@ -0,0 +1,54 @@ +import { nl as dict } from "@blocknote/core"; + +export const nl = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/pl.ts b/packages/ai/src/core/i18n/locales/pl.ts new file mode 100644 index 000000000..d83ad4693 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/pl.ts @@ -0,0 +1,54 @@ +import { pl as dict } from "@blocknote/core"; + +export const pl = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/pt.ts b/packages/ai/src/core/i18n/locales/pt.ts new file mode 100644 index 000000000..e8024ce41 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/pt.ts @@ -0,0 +1,54 @@ +import { pt as dict } from "@blocknote/core"; + +export const pt = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/ru.ts b/packages/ai/src/core/i18n/locales/ru.ts new file mode 100644 index 000000000..4b89ed14c --- /dev/null +++ b/packages/ai/src/core/i18n/locales/ru.ts @@ -0,0 +1,54 @@ +import { ru as dict } from "@blocknote/core"; + +export const ru = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/vi.ts b/packages/ai/src/core/i18n/locales/vi.ts new file mode 100644 index 000000000..2b75f9097 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/vi.ts @@ -0,0 +1,54 @@ +import { vi as dict } from "@blocknote/core"; + +export const vi = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/core/i18n/locales/zh.ts b/packages/ai/src/core/i18n/locales/zh.ts new file mode 100644 index 000000000..1cb2bb191 --- /dev/null +++ b/packages/ai/src/core/i18n/locales/zh.ts @@ -0,0 +1,54 @@ +import { zh as dict } from "@blocknote/core"; + +export const zh = { + ...dict, + formatting_toolbar: { + ...dict.formatting_toolbar, + ai: { + tooltip: "Generate content", + input_placeholder: "Enter a prompt", + }, + }, + slash_menu: { + ...dict.slash_menu, + ai_block: { + title: "AI Block", + subtext: "Block with AI generated content", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + ai: { + title: "Ask AI", + subtext: "Continue writing with AI", + aliases: ["ai", "artificial intelligence", "generate"], + group: "AI", + }, + }, + placeholders: { + ...dict.placeholders, + ai: "Enter a prompt", + }, + ai_menu: { + custom_prompt: { + title: "Custom Prompt", + subtext: "Use your query as an AI prompt", + aliases: ["", "custom prompt"], + }, + make_longer: { + title: "Make Longer", + aliases: ["make longer"], + }, + }, + ai_block_toolbar: { + show_prompt: "Generated by AI", + show_prompt_datetime_tooltip: "Generated:", + update: "Update", + updating: "Updating…", + }, + ai_inline_toolbar: { + accept: "Accept", + retry: "Retry", + updating: "Updating…", + revert: "Revert", + }, +}; diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 1863491c8..c8fe1353f 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,3 +1,2 @@ export * from "./core"; -export * from "./mantine"; export * from "./react"; diff --git a/packages/ai/src/mantine/index.tsx b/packages/ai/src/mantine/index.tsx deleted file mode 100644 index 9579e5684..000000000 --- a/packages/ai/src/mantine/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; -import { ComponentProps } from "react"; -import { - BlockNoteView as BlockNoteViewMantine, - Theme, -} from "@blocknote/mantine"; - -import { BlockNoteViewRaw } from "../react"; - -export const BlockNoteView = < - BSchema extends BlockSchema, - ISchema extends InlineContentSchema, - SSchema extends StyleSchema ->( - props: Omit< - ComponentProps>, - "theme" - > & { - theme?: - | "light" - | "dark" - | Theme - | { - light: Theme; - dark: Theme; - }; - } -) => ; diff --git a/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbar.tsx b/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbar.tsx index eb75c65c4..864714275 100644 --- a/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbar.tsx +++ b/packages/ai/src/react/components/AIBlockToolbar/AIBlockToolbar.tsx @@ -32,9 +32,9 @@ export const AIBlockToolbar = ( const [updating, setUpdating] = useState(false); return ( - + {props.children || getAIBlockToolbarItems({ ...props, updating, setUpdating })} - + ); }; diff --git a/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx b/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx index 7f795c493..19fc1f1b8 100644 --- a/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx +++ b/packages/ai/src/react/components/AIBlockToolbar/DefaultButtons/ShowPromptButton.tsx @@ -3,7 +3,7 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { useComponentsContext, useDictionary } from "@blocknote/react"; +import { useComponentsContext } from "@blocknote/react"; import { ChangeEvent, KeyboardEvent, @@ -16,6 +16,7 @@ import { RiSparkling2Fill } from "react-icons/ri"; import { aiBlockConfig } from "../../../../core/blocks/AIBlockContent/AIBlockContent"; import { mockAIReplaceBlockContent } from "../../../../core/blocks/AIBlockContent/mockAIFunctions"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../hooks/useDictionary"; import { AIBlockToolbarProps } from "../AIBlockToolbarProps"; export const ShowPromptButton = ( @@ -79,7 +80,7 @@ export const ShowPromptButton = ( return ( - {dict.ai_block_toolbar.show_prompt} - + { @@ -46,6 +47,6 @@ export const UpdateButton = ( {props.updating ? dict.ai_block_toolbar.updating : dict.ai_block_toolbar.update} - + ); }; diff --git a/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbar.tsx b/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbar.tsx index 1454be673..ed441564c 100644 --- a/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbar.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/AIInlineToolbar.tsx @@ -64,7 +64,7 @@ export const AIInlineToolbar = ( }, []); return ( - + {props.children || getAIInlineToolbarItems({ ...props, @@ -72,6 +72,6 @@ export const AIInlineToolbar = ( updating, setUpdating, })} - + ); }; diff --git a/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx index 8a8644bb5..58247189f 100644 --- a/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/AcceptButton.tsx @@ -1,7 +1,8 @@ -import { useComponentsContext, useDictionary } from "@blocknote/react"; +import { useComponentsContext } from "@blocknote/react"; import { RiCheckFill } from "react-icons/ri"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../hooks/useDictionary"; export const AcceptButton = () => { const dict = useDictionary(); @@ -14,7 +15,7 @@ export const AcceptButton = () => { } return ( - } mainTooltip={dict.ai_inline_toolbar.accept} diff --git a/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx index f3a4a9909..0ecb44372 100644 --- a/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RetryButton.tsx @@ -1,8 +1,9 @@ -import { useComponentsContext, useDictionary } from "@blocknote/react"; +import { useComponentsContext } from "@blocknote/react"; import { RiLoopLeftFill } from "react-icons/ri"; import { mockAIReplaceSelection } from "../../../../core/blocks/AIBlockContent/mockAIFunctions"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../hooks/useDictionary"; import { AIInlineToolbarProps } from "../AIInlineToolbarProps"; export const RetryButton = ( @@ -21,7 +22,7 @@ export const RetryButton = ( } return ( - } mainTooltip={dict.ai_inline_toolbar.retry} @@ -33,6 +34,6 @@ export const RetryButton = ( props.setUpdating(false); }}> {props.updating && dict.ai_inline_toolbar.updating} - + ); }; diff --git a/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx index e93b4d5f2..dbea9ad4a 100644 --- a/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx +++ b/packages/ai/src/react/components/AIInlineToolbar/DefaultButtons/RevertButton.tsx @@ -1,9 +1,10 @@ import { Block } from "@blocknote/core"; -import { useComponentsContext, useDictionary } from "@blocknote/react"; +import { useComponentsContext } from "@blocknote/react"; import { TextSelection } from "prosemirror-state"; import { RiArrowGoBackFill } from "react-icons/ri"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../hooks/useDictionary"; import { AIInlineToolbarProps } from "../AIInlineToolbarProps"; export const RevertButton = ( @@ -21,7 +22,7 @@ export const RevertButton = ( } return ( - } mainTooltip={dict.ai_inline_toolbar.revert} diff --git a/packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx b/packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx index 46d59ca87..1c7157b54 100644 --- a/packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx +++ b/packages/ai/src/react/components/FormattingToolbar/DefaultButtons/AIButton.tsx @@ -1,9 +1,10 @@ import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; -import { useComponentsContext, useDictionary } from "@blocknote/react"; +import { useComponentsContext } from "@blocknote/react"; import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react"; import { RiSparkling2Fill } from "react-icons/ri"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor"; +import { useDictionary } from "../../../hooks/useDictionary"; export const AIButton = () => { const dict = useDictionary(); @@ -48,7 +49,7 @@ export const AIButton = () => { return ( - ({ - dictKey: item.title as any, + name: item.title as any, ...item, onItemClick: async () => { editor.aiInlineToolbar.open( diff --git a/packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx index 30c1b0f0d..1a62492ce 100644 --- a/packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx +++ b/packages/ai/src/react/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx @@ -24,10 +24,10 @@ export function getDefaultReactSlashMenuItems< >(editor: BlockNoteEditor): DefaultReactSuggestionItem[] { const items: DefaultReactSuggestionItem[] = getDefaultCoreSlashMenuItems(editor); - const insertionIndex = items.findIndex((item) => item.dictKey === "emoji"); + const insertionIndex = items.findIndex((item) => item.name === "emoji"); items.splice(insertionIndex, 0, { - dictKey: "ai", + name: "ai", onItemClick: () => editor.openSelectionMenu("`"), ...editor.dictionary.slash_menu.ai, icon: , @@ -35,7 +35,7 @@ export function getDefaultReactSlashMenuItems< if (checkDefaultBlockTypeInSchema("ai", editor)) { items.splice(insertionIndex, 0, { - dictKey: "ai_block", + name: "ai_block", onItemClick: () => { insertOrUpdateBlock(editor, { type: "ai", diff --git a/packages/ai/src/react/editor/BlockNoteView.tsx b/packages/ai/src/react/editor/BlockNoteView.tsx deleted file mode 100644 index fca74e393..000000000 --- a/packages/ai/src/react/editor/BlockNoteView.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; -import { - BlockNoteViewComponent, - BlockNoteViewProps as BlockNoteViewCoreProps, - BlockNoteViewRaw as BlockNoteViewCoreRaw, -} from "@blocknote/react"; -import { - BlockNoteDefaultUI, - BlockNoteDefaultUIProps, -} from "./BlockNoteDefaultUI"; -import React, { ComponentProps } from "react"; - -export type BlockNoteViewProps< - BSchema extends BlockSchema, - ISchema extends InlineContentSchema, - SSchema extends StyleSchema -> = BlockNoteViewCoreProps & BlockNoteDefaultUIProps; - -export const BlockNoteViewRaw = React.forwardRef((props, ref) => ( - - - -)) as < - BSchema extends BlockSchema, - ISchema extends InlineContentSchema, - SSchema extends StyleSchema ->( - props: ComponentProps< - typeof BlockNoteViewComponent - > & { - ref?: React.ForwardedRef; - } -) => ReturnType>; diff --git a/packages/ai/src/react/hooks/useBlockNoteEditor.ts b/packages/ai/src/react/hooks/useBlockNoteEditor.ts index be24f3824..671c736a2 100644 --- a/packages/ai/src/react/hooks/useBlockNoteEditor.ts +++ b/packages/ai/src/react/hooks/useBlockNoteEditor.ts @@ -17,5 +17,9 @@ export function useBlockNoteEditor< >( _schema?: BlockNoteSchema ): BlockNoteEditor { - return useBlockNoteEditorCore(_schema) as any; + return useBlockNoteEditorCore(_schema) as BlockNoteEditor< + BSchema, + ISchema, + SSchema + >; } diff --git a/packages/ai/src/react/hooks/useCreateBlockNote.tsx b/packages/ai/src/react/hooks/useCreateBlockNote.tsx index d80d0c255..ec4ad984a 100644 --- a/packages/ai/src/react/hooks/useCreateBlockNote.tsx +++ b/packages/ai/src/react/hooks/useCreateBlockNote.tsx @@ -1,5 +1,4 @@ import { - BlockNoteEditorOptions, BlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, @@ -8,7 +7,11 @@ import { } from "@blocknote/core"; import { DependencyList, useMemo } from "react"; -import { BlockNoteEditor, DefaultBlockSchema } from "../../core"; +import { + BlockNoteEditor, + BlockNoteEditorOptions, + DefaultBlockSchema, +} from "../../core"; export const useCreateBlockNote = < BSchema extends BlockSchema = DefaultBlockSchema, diff --git a/packages/ai/src/react/hooks/useDictionary.ts b/packages/ai/src/react/hooks/useDictionary.ts new file mode 100644 index 000000000..9b433da4e --- /dev/null +++ b/packages/ai/src/react/hooks/useDictionary.ts @@ -0,0 +1,7 @@ +import { useDictionary as useCoreDictionary } from "@blocknote/react"; + +import { Dictionary } from "../../core/i18n/dictionary"; + +export function useDictionary(): Dictionary { + return useCoreDictionary() as Dictionary; +} diff --git a/packages/ai/src/react/index.ts b/packages/ai/src/react/index.ts index 040fed92b..1462f82c7 100644 --- a/packages/ai/src/react/index.ts +++ b/packages/ai/src/react/index.ts @@ -20,7 +20,6 @@ export * from "./components/SuggestionMenu/getDefaultAIMenuItems"; export * from "./components/SuggestionMenu/getDefaultReactSlashMenuItems"; export * from "./editor/BlockNoteDefaultUI"; -export * from "./editor/BlockNoteView"; export * from "./hooks/useBlockNoteEditor"; export * from "./hooks/useCreateBlockNote"; diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx index 350be21bf..3b7581d3d 100644 --- a/packages/ariakit/src/index.tsx +++ b/packages/ariakit/src/index.tsx @@ -46,15 +46,7 @@ import { ToolbarSelect } from "./toolbar/ToolbarSelect"; import "./style.css"; export const components: Components = { - AIBlockToolbar: { - Root: Toolbar, - Button: ToolbarButton, - }, - AIInlineToolbar: { - Root: Toolbar, - Button: ToolbarButton, - }, - FormattingToolbar: { + Toolbar: { Root: Toolbar, Button: ToolbarButton, Select: ToolbarSelect, @@ -72,10 +64,6 @@ export const components: Components = { EmptyItem: GridSuggestionMenuEmptyItem, Loader: GridSuggestionMenuLoader, }, - LinkToolbar: { - Root: Toolbar, - Button: ToolbarButton, - }, SideMenu: { Root: SideMenu, Button: SideMenuButton, diff --git a/packages/ariakit/src/toolbar/Toolbar.tsx b/packages/ariakit/src/toolbar/Toolbar.tsx index 20b588b4e..6afa56131 100644 --- a/packages/ariakit/src/toolbar/Toolbar.tsx +++ b/packages/ariakit/src/toolbar/Toolbar.tsx @@ -4,8 +4,7 @@ import { assertEmpty, mergeCSSClasses } from "@blocknote/core"; import { ComponentProps } from "@blocknote/react"; import { forwardRef } from "react"; -type ToolbarProps = ComponentProps["FormattingToolbar"]["Root"] & - ComponentProps["LinkToolbar"]["Root"]; +type ToolbarProps = ComponentProps["Toolbar"]["Root"]; export const Toolbar = forwardRef( (props, ref) => { diff --git a/packages/ariakit/src/toolbar/ToolbarButton.tsx b/packages/ariakit/src/toolbar/ToolbarButton.tsx index d0027be9c..a2e273ab3 100644 --- a/packages/ariakit/src/toolbar/ToolbarButton.tsx +++ b/packages/ariakit/src/toolbar/ToolbarButton.tsx @@ -9,8 +9,7 @@ import { assertEmpty, isSafari, mergeCSSClasses } from "@blocknote/core"; import { ComponentProps } from "@blocknote/react"; import { forwardRef } from "react"; -type ToolbarButtonProps = ComponentProps["FormattingToolbar"]["Button"] & - ComponentProps["LinkToolbar"]["Button"]; +type ToolbarButtonProps = ComponentProps["Toolbar"]["Button"]; /** * Helper for basic buttons that show in the formatting toolbar. diff --git a/packages/ariakit/src/toolbar/ToolbarSelect.tsx b/packages/ariakit/src/toolbar/ToolbarSelect.tsx index 83d0cf8bd..101bfcf02 100644 --- a/packages/ariakit/src/toolbar/ToolbarSelect.tsx +++ b/packages/ariakit/src/toolbar/ToolbarSelect.tsx @@ -14,7 +14,7 @@ import { forwardRef } from "react"; export const ToolbarSelect = forwardRef< HTMLDivElement, - ComponentProps["FormattingToolbar"]["Select"] + ComponentProps["Toolbar"]["Select"] >((props, ref) => { const { className, items, isDisabled, ...rest } = props; diff --git a/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts b/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts index 4387ac768..b4471d936 100644 --- a/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts +++ b/packages/core/src/extensions/SuggestionMenu/DefaultSuggestionItem.ts @@ -1,7 +1,5 @@ -import type { Dictionary } from "../../i18n/dictionary"; - export type DefaultSuggestionItem = { - dictKey: keyof Dictionary["slash_menu"]; + name: string; title: string; onItemClick: () => void; subtext?: string; diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 58362ccf7..2001d86de 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -91,7 +91,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-1"), - dictKey: "heading", + name: "heading", ...editor.dictionary.slash_menu.heading, }, { @@ -102,7 +102,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-2"), - dictKey: "heading_2", + name: "heading_2", ...editor.dictionary.slash_menu.heading_2, }, { @@ -113,7 +113,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-3"), - dictKey: "heading_3", + name: "heading_3", ...editor.dictionary.slash_menu.heading_3, } ); @@ -127,7 +127,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Shift-7"), - dictKey: "numbered_list", + name: "numbered_list", ...editor.dictionary.slash_menu.numbered_list, }); } @@ -140,7 +140,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Shift-8"), - dictKey: "bullet_list", + name: "bullet_list", ...editor.dictionary.slash_menu.bullet_list, }); } @@ -153,7 +153,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Shift-9"), - dictKey: "check_list", + name: "check_list", ...editor.dictionary.slash_menu.check_list, }); } @@ -166,7 +166,7 @@ export function getDefaultSlashMenuItems< }); }, badge: formatKeyboardShortcut("Mod-Alt-0"), - dictKey: "paragraph", + name: "paragraph", ...editor.dictionary.slash_menu.paragraph, }); } @@ -190,7 +190,7 @@ export function getDefaultSlashMenuItems< }); }, badge: undefined, - dictKey: "table", + name: "table", ...editor.dictionary.slash_menu.table, }); } @@ -209,7 +209,7 @@ export function getDefaultSlashMenuItems< }) ); }, - dictKey: "image", + name: "image", ...editor.dictionary.slash_menu.image, }); } @@ -228,7 +228,7 @@ export function getDefaultSlashMenuItems< }) ); }, - dictKey: "video", + name: "video", ...editor.dictionary.slash_menu.video, }); } @@ -247,7 +247,7 @@ export function getDefaultSlashMenuItems< }) ); }, - dictKey: "audio", + name: "audio", ...editor.dictionary.slash_menu.audio, }); } @@ -266,14 +266,14 @@ export function getDefaultSlashMenuItems< }) ); }, - dictKey: "image", + name: "image", ...editor.dictionary.slash_menu.file, }); } items.push({ onItemClick: () => editor.openSelectionMenu(":"), - dictKey: "emoji", + name: "emoji", ...editor.dictionary.slash_menu.emoji, }); diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 6750b6a2b..7c81fd8e7 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -90,18 +90,6 @@ export const ar: Dictionary = { aliases: ["ملف", "تحميل", "تضمين", "وسائط", "رابط"], group: "وسائط", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "الرموز التعبيرية", subtext: "تُستخدم لإدراج رمز تعبيري", @@ -109,24 +97,12 @@ export const ar: Dictionary = { group: "آخرون", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "أدخل نصًا أو اكتب '/' للأوامر", heading: "عنوان", bulletListItem: "قائمة", numberedListItem: "قائمة", checkListItem: "قائمة", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -273,10 +249,6 @@ export const ar: Dictionary = { align_justify: { tooltip: "ضبط النص", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -316,18 +288,6 @@ export const ar: Dictionary = { url_placeholder: "تحرير الرابط", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index dd4723f3c..b7dc7ac43 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -104,18 +104,6 @@ export const en = { aliases: ["file", "upload", "embed", "media", "url"], group: "Media", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Emoji", subtext: "Search for and insert an emoji", @@ -123,24 +111,12 @@ export const en = { group: "Others", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Enter text or type '/' for commands", heading: "Heading", bulletListItem: "List", numberedListItem: "List", checkListItem: "List", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -287,10 +263,6 @@ export const en = { align_justify: { tooltip: "Justify text", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -330,18 +302,6 @@ export const en = { url_placeholder: "Edit URL", }, }, - ai_block_toolbar: { - show_prompt: "Generated by AI", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index 020168d54..bd9e073aa 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -105,18 +105,6 @@ export const fr: Dictionary = { aliases: ["fichier", "téléverser", "intégrer", "média", "url"], group: "Média", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Emoji", subtext: "Utilisé pour insérer un emoji", @@ -124,24 +112,12 @@ export const fr: Dictionary = { group: "Autres", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Entrez du texte ou tapez '/' pour les commandes", heading: "Titre", bulletListItem: "Liste", numberedListItem: "Liste", checkListItem: "Liste", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -288,10 +264,6 @@ export const fr: Dictionary = { align_justify: { tooltip: "Justifier le texte", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -331,18 +303,6 @@ export const fr: Dictionary = { url_placeholder: "Modifier l'URL", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/index.ts b/packages/core/src/i18n/locales/index.ts index 94f5506bb..ad4ec1473 100644 --- a/packages/core/src/i18n/locales/index.ts +++ b/packages/core/src/i18n/locales/index.ts @@ -7,6 +7,6 @@ export * from "./ko"; export * from "./nl"; export * from "./pl"; export * from "./pt"; +export * from "./ru"; export * from "./vi"; export * from "./zh"; -export * from "./ru"; \ No newline at end of file diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index 5bd294bb1..4b4ee544d 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -98,18 +98,6 @@ export const is: Dictionary = { aliases: ["skrá", "hlaða upp", "fella inn", "miðill", "url"], group: "Miðlar", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Emoji", subtext: "Notað til að setja inn smámynd", @@ -117,24 +105,12 @@ export const is: Dictionary = { group: "Annað", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Sláðu inn texta eða skrifaðu '/' fyrir skipanir", heading: "Fyrirsögn", bulletListItem: "Listi", numberedListItem: "Listi", checkListItem: "Listi", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -280,10 +256,6 @@ export const is: Dictionary = { align_justify: { tooltip: "Jafna texta", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -323,18 +295,6 @@ export const is: Dictionary = { url_placeholder: "Breyta URL", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index ca11d553f..91c7d983d 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -125,18 +125,6 @@ export const ja: Dictionary = { aliases: ["file", "upload", "embed", "media", "url", "ファイル"], group: "メディア", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "絵文字", subtext: "絵文字を挿入するために使用します", @@ -144,24 +132,12 @@ export const ja: Dictionary = { group: "その他", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "テキストを入力するか'/' を入力してコマンド選択", heading: "見出し", bulletListItem: "リストを追加", numberedListItem: "リストを追加", checkListItem: "リストを追加", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -308,10 +284,6 @@ export const ja: Dictionary = { align_justify: { tooltip: "両端揃え", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -351,18 +323,6 @@ export const ja: Dictionary = { url_placeholder: "URLを編集", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 83f86299a..9c7f6bf92 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -109,18 +109,6 @@ export const ko: Dictionary = { aliases: ["file", "upload", "embed", "media", "파일", "url"], group: "미디어", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "이모지", subtext: "이모지 삽입용으로 사용됩니다", @@ -137,24 +125,12 @@ export const ko: Dictionary = { group: "기타", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "텍스트를 입력하거나 /를 입력하여 명령을 입력하세요.", heading: "제목", bulletListItem: "목록", numberedListItem: "목록", checkListItem: "목록", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -301,10 +277,6 @@ export const ko: Dictionary = { align_justify: { tooltip: "텍스트 양쪽 맞춤", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -344,18 +316,6 @@ export const ko: Dictionary = { url_placeholder: "URL 수정", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index b03445cb3..782645c1a 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -100,18 +100,6 @@ export const nl: Dictionary = { aliases: ["bestand", "upload", "insluiten", "media", "url"], group: "Media", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Emoji", subtext: "Gebruikt voor het invoegen van een emoji", @@ -124,24 +112,12 @@ export const nl: Dictionary = { group: "Overig", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Voer tekst in of type '/' voor commando's", heading: "Kop", bulletListItem: "Lijst", numberedListItem: "Lijst", checkListItem: "Lijst", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -287,10 +263,6 @@ export const nl: Dictionary = { align_justify: { tooltip: "Tekst uitvullen", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -330,18 +302,6 @@ export const nl: Dictionary = { url_placeholder: "Bewerk URL", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index b03397692..89aedc0a5 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -90,18 +90,6 @@ export const pl: Dictionary = { aliases: ["plik", "wrzuć", "wstaw", "media", "url"], group: "Media", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Emoji", subtext: "Używane do wstawiania emoji", @@ -109,24 +97,12 @@ export const pl: Dictionary = { group: "Inne", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Wprowadź tekst lub wpisz '/' aby użyć poleceń", heading: "Nagłówek", bulletListItem: "Lista", numberedListItem: "Lista", checkListItem: "Lista", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -272,10 +248,6 @@ export const pl: Dictionary = { align_justify: { tooltip: "Wyjustuj tekst", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -315,18 +287,6 @@ export const pl: Dictionary = { url_placeholder: "Edytuj URL", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 61d25328c..6ee76d166 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -97,18 +97,6 @@ export const pt: Dictionary = { aliases: ["arquivo", "upload", "incorporar", "mídia", "url"], group: "Mídia", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Emoji", subtext: "Usado para inserir um emoji", @@ -116,24 +104,12 @@ export const pt: Dictionary = { group: "Outros", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Digite texto ou use '/' para comandos", heading: "Título", bulletListItem: "Lista", numberedListItem: "Lista", checkListItem: "Lista", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -280,10 +256,6 @@ export const pt: Dictionary = { align_justify: { tooltip: "Justificar texto", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -323,18 +295,6 @@ export const pt: Dictionary = { url_placeholder: "Editar URL", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index 6a3ba7197..51a8f2653 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -132,18 +132,6 @@ export const ru: Dictionary = { aliases: ["file", "upload", "embed", "media", "url", "загрузка", "файл"], group: "Медиа", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Эмодзи", subtext: "Используется для вставки эмодзи", @@ -151,24 +139,12 @@ export const ru: Dictionary = { group: "Прочее", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Ведите текст или введите «/» для команд", heading: "Заголовок", bulletListItem: "Список", numberedListItem: "Список", checkListItem: "Список", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -315,10 +291,6 @@ export const ru: Dictionary = { align_justify: { tooltip: "По середине текст", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -358,18 +330,6 @@ export const ru: Dictionary = { url_placeholder: "Изменить URL", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index 098f8832d..f188196dd 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -97,18 +97,6 @@ export const vi: Dictionary = { aliases: ["tep", "tai-len", "nhung", "media", "url"], group: "Phương tiện", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "Biểu tượng cảm xúc", subtext: "Dùng để chèn biểu tượng cảm xúc", @@ -123,24 +111,12 @@ export const vi: Dictionary = { group: "Khác", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "Nhập văn bản hoặc gõ '/' để thêm định dạng", heading: "Tiêu đề", bulletListItem: "Danh sách", numberedListItem: "Danh sách", checkListItem: "Danh sách", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -287,10 +263,6 @@ export const vi: Dictionary = { align_justify: { tooltip: "Căn đều văn bản", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -330,18 +302,6 @@ export const vi: Dictionary = { url_placeholder: "Chỉnh sửa URL", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index 9d66ec8a1..6c2835659 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -130,18 +130,6 @@ export const zh: Dictionary = { aliases: ["文件", "上传", "file", "嵌入", "媒体", "url"], group: "媒体", }, - ai_block: { - title: "AI Block", - subtext: "Block with AI generated content", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, - ai: { - title: "Ask AI", - subtext: "Continue writing with AI", - aliases: ["ai", "artificial intelligence", "generate"], - group: "AI", - }, emoji: { title: "表情符号", subtext: "用于插入表情符号", @@ -157,24 +145,12 @@ export const zh: Dictionary = { group: "其他", }, }, - ai_menu: { - custom_prompt: { - title: "Custom Prompt", - subtext: "Use your query as an AI prompt", - aliases: ["", "custom prompt"], - }, - make_longer: { - title: "Make Longer", - aliases: ["make longer"], - }, - }, placeholders: { default: "输入 '/' 以使用命令", heading: "标题", bulletListItem: "列表", numberedListItem: "列表", checkListItem: "列表", - ai: "Enter a prompt", }, file_blocks: { image: { @@ -321,10 +297,6 @@ export const zh: Dictionary = { align_justify: { tooltip: "文本对齐", }, - ai: { - tooltip: "Generate content", - input_placeholder: "Enter a prompt", - }, }, file_panel: { upload: { @@ -364,18 +336,6 @@ export const zh: Dictionary = { url_placeholder: "编辑链接地址", }, }, - ai_block_toolbar: { - show_prompt: "Show prompt", - show_prompt_datetime_tooltip: "Generated:", - update: "Update", - updating: "Updating…", - }, - ai_inline_toolbar: { - accept: "Accept", - retry: "Retry", - updating: "Updating…", - revert: "Revert", - }, generic: { ctrl_shortcut: "Ctrl", }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b02f28a77..b37d70479 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,6 +29,7 @@ export * from "./extensions/SuggestionMenu/SuggestionPlugin"; export * from "./extensions/SuggestionMenu/getDefaultSlashMenuItems"; export * from "./extensions/SuggestionMenu/getDefaultEmojiPickerItems"; export * from "./extensions/TableHandles/TableHandlesPlugin"; +export * from "./i18n/locales"; export * from "./i18n/dictionary"; export * from "./schema"; export * from "./util/browser"; diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index 3c708893a..9a2a69ae7 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -55,15 +55,7 @@ export * from "./BlockNoteTheme"; export * from "./defaultThemes"; export const components: Components = { - AIBlockToolbar: { - Root: Toolbar, - Button: ToolbarButton, - }, - AIInlineToolbar: { - Root: Toolbar, - Button: ToolbarButton, - }, - FormattingToolbar: { + Toolbar: { Root: Toolbar, Button: ToolbarButton, Select: ToolbarSelect, @@ -81,10 +73,6 @@ export const components: Components = { EmptyItem: GridSuggestionMenuEmptyItem, Loader: GridSuggestionMenuLoader, }, - LinkToolbar: { - Root: Toolbar, - Button: ToolbarButton, - }, SideMenu: { Root: SideMenu, Button: SideMenuButton, diff --git a/packages/mantine/src/toolbar/Toolbar.tsx b/packages/mantine/src/toolbar/Toolbar.tsx index 9ad042e85..a9c2b07d2 100644 --- a/packages/mantine/src/toolbar/Toolbar.tsx +++ b/packages/mantine/src/toolbar/Toolbar.tsx @@ -5,8 +5,7 @@ import { ComponentProps } from "@blocknote/react"; import { mergeRefs, useFocusTrap, useFocusWithin } from "@mantine/hooks"; import { forwardRef } from "react"; -type ToolbarProps = ComponentProps["FormattingToolbar"]["Root"] & - ComponentProps["LinkToolbar"]["Root"]; +type ToolbarProps = ComponentProps["Toolbar"]["Root"]; export const Toolbar = forwardRef( (props, ref) => { diff --git a/packages/mantine/src/toolbar/ToolbarButton.tsx b/packages/mantine/src/toolbar/ToolbarButton.tsx index 882c4adb3..6ba92931d 100644 --- a/packages/mantine/src/toolbar/ToolbarButton.tsx +++ b/packages/mantine/src/toolbar/ToolbarButton.tsx @@ -22,8 +22,7 @@ export const TooltipContent = (props: { ); -type ToolbarButtonProps = ComponentProps["FormattingToolbar"]["Button"] & - ComponentProps["LinkToolbar"]["Button"]; +type ToolbarButtonProps = ComponentProps["Toolbar"]["Button"]; /** * Helper for basic buttons that show in the formatting toolbar. diff --git a/packages/mantine/src/toolbar/ToolbarSelect.tsx b/packages/mantine/src/toolbar/ToolbarSelect.tsx index 32638ca5a..6e57b178b 100644 --- a/packages/mantine/src/toolbar/ToolbarSelect.tsx +++ b/packages/mantine/src/toolbar/ToolbarSelect.tsx @@ -12,7 +12,7 @@ import { HiChevronDown } from "react-icons/hi"; // TODO: Turn into select? export const ToolbarSelect = forwardRef< HTMLDivElement, - ComponentProps["FormattingToolbar"]["Select"] + ComponentProps["Toolbar"]["Select"] >((props, ref) => { const { className, items, isDisabled, ...rest } = props; diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.tsx index 1be42b3ee..a8af0d144 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.tsx @@ -107,7 +107,7 @@ export const BasicTextStyleButton =