DEV Community

Cover image for 🌟Extending the BlockNote Editor: A Custom Formatting Bar with AI-Powered Features 🤖
MrSuperCraft
MrSuperCraft

Posted on

🌟Extending the BlockNote Editor: A Custom Formatting Bar with AI-Powered Features 🤖

The BlockNote editor, often recognized for its Notion-like editing experience, provides developers with the flexibility to customize and extend its functionality. As part of a personal project aimed at improving documentation workflows, I worked on extending BlockNote’s formatting bar with an AI-powered feature. This new functionality integrates with an AI/ML API to support automated content writing within documentation-like blocks. In this article, I’ll share my experience and guide you through implementing a similar feature in your own project.


Setting Up BlockNote in Your Project

Before diving into customizations, you need to set up the BlockNote editor in your environment. For the basic setup, let's assume that we don't use any CRUD functionality and this is a completely independent feature within a web app.
There are a couple of steps to follow in order to create the proper BlockNote editor setup:

  1. Install BlockNote - related packages:
npm install @blocknote/core @blocknote/react @blocknote/mantine 
Enter fullscreen mode Exit fullscreen mode
  1. Initialize the Editor: Set up your React application:
"use client"

import type React from "react"
import { useEffect, useState } from "react"
import "@blocknote/mantine/style.css"
import { BlockNoteView } from "@blocknote/mantine"


const Editor: React.FC<EditorProps> = ({ initialContent, editable }) => {


    const editor = useCreateBlockNote({
      // Feel free to include props from the official docs like initialContent for preloaded blocks
    })

    // This is just a simple article display, I used prose on the article for improved styling.
    return (
        <div className="flex-grow overflow-y-auto">
                <article className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto py-8">
                    <BlockNoteView editor={editor} />
                </article>
        </div>
    )
}

export default Editor
Enter fullscreen mode Exit fullscreen mode

Once set up, you’re ready to start building custom features for the formatting bar.


Adding a Custom AI Button to the Formatting Bar

For my project, I wanted the AI button to:

  • Send the selected text block(s) to AI/ML API.
  • Process the text and generate automated content (e.g., summaries or extended explanations).
  • Replace the processed text back into the editor seamlessly.

Step 1: Extending the Formatting Bar

To customize the formatting bar, you can define your own button and integrate it into the existing toolbar. Here’s an example:

'use client';

import { useBlockNoteEditor, useComponentsContext } from "@blocknote/react"
import { Sparkles, Loader } from "lucide-react"
import { useState } from 'react';

export function AIButton() {
    const editor = useBlockNoteEditor()
    const Components = useComponentsContext()!
    const [isLoading, setIsLoading] = useState(false);



    // The function to handle the response, takes the AI content (chose to use Markdown exclusively) and uses it.
    const handleAIResponse = async (content: string) => {
        // blocks -> the text found from the selection of the user (prompt)
        const blocks = editor.getSelection()?.blocks ?? [];
        // blocksFromMarkdown -> the processed and formatted blocks after a markdown conversion to the BlockNote format.
        const blocksFromMarkdown = await editor.tryParseMarkdownToBlocks(content);

// Replaces the blocks from the selection with the new blocks
        editor.replaceBlocks(blocks, blocksFromMarkdown);
    }

// My call to the API via fetch together with the selected text as a prompt.
    const callAI = async () => {
        setIsLoading(true);
        const selectedText = editor.getSelectedText();
        if (selectedText) {
            try {
// /api/ai handles the call with a response containing the content.
// Feel free to use any AI SDK of your choosing.
                const response = await fetch('/api/ai', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ prompt: selectedText }),
                })
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                const data = await response.json()
                handleAIResponse(data.content)
            } catch (error) {
                console.error('Error fetching AI response:', error)
            } finally {
                setIsLoading(false);
            }
        }
    }

    return (
        <>
            <Components.FormattingToolbar.Button
                mainTooltip={"AI Assistant"}
                onClick={callAI}
                isDisabled={isLoading}
            >
                {isLoading ? <Loader className="w-4 h-4 animate-spin" /> : <>AI <Sparkles className="ml-2 w-4 h-4" /></>}
            </Components.FormattingToolbar.Button>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Adding the Button to the Toolbar

You can add the custom AI button to the editor’s toolbar using BlockNote’s customization capabilities. I also added some props, debouncing for saved edits of the content in a DB and custom placeholders:

"use client"

import React, { useEffect, useState } from "react"
import "@blocknote/mantine/style.css"
import { BlockNoteView } from "@blocknote/mantine"
import {
    UnnestBlockButton,
    ColorStyleButton,
    FileReplaceButton,
    FileCaptionButton,
    BasicTextStyleButton,
    BlockTypeSelect,
    FormattingToolbarController,
    FormattingToolbar,
    TextAlignButton,
    NestBlockButton,
    CreateLinkButton,
    useCreateBlockNote,
} from "@blocknote/react"
import { AIButton } from "./AIButton"
import { locales, type PartialBlock } from "@blocknote/core"
import debounce from "lodash.debounce"
import { toast } from "sonner"
import { Loader } from "lucide-react"

interface EditorProps {
    onChange: (value: string) => void
    initialContent?: string;
    editable?: boolean
}

const Editor: React.FC<EditorProps> = ({ onChange, initialContent, editable }) => {
    const locale = locales["en"]
    const [loading, setLoading] = useState(true)

    const editor = useCreateBlockNote({
        initialContent: initialContent
            ? (JSON.parse(initialContent) as PartialBlock[])
            : undefined,
        dictionary: {
            ...locale,
            placeholders: {
                ...locale.placeholders,
                default: "Welcome to DocsMaker! Press '/' for commands",
            },
        },
    })

    useEffect(() => {
        const debouncedOnChange = debounce(() => {
            const newContent = JSON.stringify(editor.document, null, 2)
            onChange(newContent)
            toast.success("Content saved!")
        }, 1000)

        editor.onEditorContentChange(debouncedOnChange)

        return () => {
            debouncedOnChange.cancel()
        }
    }, [editor, onChange])

    useEffect(() => {
        const timer = setTimeout(() => {
            setLoading(false)
        }, 500)

        return () => {
            clearTimeout(timer)
        }
    }, [])

    return (
        <div className="flex-grow overflow-y-auto">
            {loading ? (
                <div className="flex items-center justify-center py-32">
                    <Loader size={32} className="animate-spin" />
                </div>
            ) : (
                <article className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto py-8">
 {/* make sure that formattingToolbar is false and that the FormattingToolbarController component is added with the buttons you want to include */}
                    <BlockNoteView editor={editor} formattingToolbar={false} editable={editable}>
                        <FormattingToolbarController
                            formattingToolbar={() => (
                                <FormattingToolbar>
                                    <BlockTypeSelect key={"blockTypeSelect"} />
{/* Our personal addition! */}
                                    {editable && <AIButton key={"aiButton"} />}
                                    <FileCaptionButton key={"fileCaptionButton"} />
                                    <FileReplaceButton key={"replaceFileButton"} />
                                    <BasicTextStyleButton basicTextStyle={"bold"} key={"boldStyleButton"} />
                                    <BasicTextStyleButton basicTextStyle={"italic"} key={"italicStyleButton"} />
                                    <BasicTextStyleButton basicTextStyle={"underline"} key={"underlineStyleButton"} />
                                    <BasicTextStyleButton basicTextStyle={"strike"} key={"strikeStyleButton"} />
                                    <BasicTextStyleButton key={"codeStyleButton"} basicTextStyle={"code"} />
                                    <TextAlignButton textAlignment={"left"} key={"textAlignLeftButton"} />
                                    <TextAlignButton textAlignment={"center"} key={"textAlignCenterButton"} />
                                    <TextAlignButton textAlignment={"right"} key={"textAlignRightButton"} />
                                    <ColorStyleButton key={"colorStyleButton"} />
                                    <NestBlockButton key={"nestBlockButton"} />
                                    <UnnestBlockButton key={"unnestBlockButton"} />
                                    <CreateLinkButton key={"createLinkButton"} />
                                </FormattingToolbar>
                            )}
                        />
                    </BlockNoteView>
                </article>
            )}
        </div>
    )
}

export default Editor

Enter fullscreen mode Exit fullscreen mode

Now, your editor includes a custom AI button that integrates with the toolbar and enables AI-powered content creation.


Personal Reflection and Potential Improvements

Working on this feature taught me a lot about enhancing user experience in editor - like tools. The ability to dynamically generate content not only saves time but also empowers users to focus on higher-level tasks. Challenges I faced included fine-tuning the setup to fit my use case, as well as ensuring a seamless integration with BlockNote’s specific schemas and requirements for extending the editor's functionality.

To make this feature more robust, I’m exploring options like:

  • Adding support for multiple AI models.
  • Highlighting the AI-generated content for easy review and editing.
  • Handling confirmation prompts from the user and difference display with acceptance / rejection and retry options.

Thanks for reading, and happy coding! (っ◕‿◕)っ
Feel free to reach out with any questions or thoughts. Enjoy building amazing things!

Top comments (0)