How to build your own AI suggestions custom plugin in your TinyMce react editor
AI suggestion plugin is a paid feature of tinymce rich text editor implement the feature absolutely free in your react app
Introduction
TinyMCE is a powerful, feature-rich JavaScript-based WYSIWYG (What You See Is What You Get) editor that enables users to create, edit, and format text content in a web-based environment. It is widely used in content management systems (CMS), blog platforms, and web applications where rich text editing capabilities are required.
Importance of AI suggestions in text editors
Boosts Productivity: Speeds up writing with predictive text, autocomplete, and grammar checks.
Improves Content Quality: Enhances clarity, readability, and consistency through real-time suggestions.
Personalization: Adapts to user preferences, writing style, and context for tailored recommendations.
Enhances Creativity: Suggests synonyms, alternative phrasing, and ideas to inspire better content.
SEO Optimization: Offers keywords, metadata, and content scoring for improved visibility.
Multilingual Support: Aids non-native speakers with grammar, translations, and phrasing.
Real-Time Feedback: Instantly corrects errors and aligns content with objectives.
Purpose and scope of the article
In this article, you will get to know about how you can build your own AI suggestions plugin for free the plugin is already there in tinymce plugins list but it’s paid. Here’s a step by step guide to build this plugin into your project:
Step 1: Setting Up Your TinyMCE React Environment
npm install --save @tinymce/tinymce-react
check whether the dependency is installed successfully or not into your package.json file
Now, import the Editor component into your react app
Complete the basic setup for the editor →
import React from "react";
import { Editor } from "@tinymce/tinymce-react";
const TinyMCEEditor = () => {
const handleEditorChange = (content, editor) => {
console.log("Content was updated:", content);
};
return (
<Editor
apiKey="your-tinymce-api-key" // Replace with your TinyMCE API key
initialValue="<p>This is the initial content of the editor</p>"
init={{
height: 500,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar:
'undo redo | formatselect | bold italic backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help'
}}
onEditorChange={handleEditorChange}
/>
);
};
export default TinyMCEEditor;
Step 2: Create an input component where the user will send their prompt to the AI
import React, { useState, useEffect } from "react";
import {
connectWithUserId,
socket,
} from "../../websocket/getAutocompleteSuggestion";
import { useForm } from "react-hook-form";
import { useSelector } from "react-redux";
const AiHelper = ({editorInstance}) => {
const {
register,
handleSubmit,
setValue,
formState: { isSubmitting },
} = useForm();
const [suggestion, setSuggestion] = useState("");
const [disabled, setDisabled] = useState(false);
const [abortBtnDisable, setAbortButtonDisable] = useState(true);
const [currentPrompt, setCurrentPrompt] = useState({});
const [loading, setLoading] = useState(false)
const userData = useSelector((state) => state.auth.userData);
const suggestionContainer = useRef(null);
const submit = (data) => {
setLoading(true)
setCurrentPrompt(data);
socket.emit("prompt", data.prompt);
setSuggestion("");
setValue("prompt", "");
};
const handleAbort = () => {
socket.emit("abort", true);
};
const handleTryAgain = () => {
console.log("currentPrompt ", currentPrompt);
submit(currentPrompt);
};
const handleInsert = () => {
const data = document.querySelector(".suggestion").innerHTML;
if (editorInstance) {
editorInstance.insertContent(data);
}
setIsActive(false);
};
useEffect(() => {
connectWithUserId(userData.$id);
}, [userData]);
useEffect(() => {
socket.on("suggestion", (data) => {
setLoading(false)
setSuggestion((prev) => prev + data);
setDisabled(true);
setAbortButtonDisable(false);
});
socket.on("complete", (data) => {
if (data) {
setDisabled(false);
setAbortButtonDisable(true);
}
});
const container = suggestionContainer.current;
if (container) {
container.scrollTop = container.scrollHeight;
}
return () => {
socket.off("suggestion");
socket.off("complete");
};
}, [suggestion]);
return (
<div className="w-full bg-white rounded-lg px-4 py-2 absolute z-10 md:bottom-14 bottom-72 md:left-10 space-y-2 drop-shadow-[0_0_24px_rgba(8,23,53,0.16)]">
{
loading && <div className="absolute w-full h-full bg-gray-300 opacity-80 flex justify-center items-center rounded-lg left-0 top-0 overflow-hidden space-x-1">
<span className="block w-2 h-2 bg-black rounded-full animate-bounce "></span>
<span className="block w-2 h-2 bg-black rounded-full animate-bounce delay-200"></span>
<span className="block w-2 h-2 bg-black rounded-full animate-bounce delay-500"></span>
</div>
}
<div className="flex flex-col justify-between">
<div className="flex justify-between rounded-md mb-2">
<h1 className="text-xl">AI Assistant</h1>
<Button
className="w-8 rounded-md py-1 bg-transparent hover:bg-slate-200"
onClick={(e) => {
e.preventDefault();
setIsActive(false);
}}
>
<i class="fa-solid fa-xmark text-black text-lg"></i>
</Button>
</div>
{suggestion && (
<>
<div
ref={suggestionContainer}
className=" bg-white mt-2 p-4 max-h-60 overflow-y-scroll border border-gray-200 rounded-md "
>
<ReactMarkdown className="prose text-left suggestion">
{suggestion}
</ReactMarkdown>
</div>
<div className=" flex gap-2 mt-2">
<Button
className={`px-4 py-2 font-bold rounded-md text-sm ${
disabled
? "bg-blue-700 text-opacity-40 cursor-not-allowed"
: "hover:bg-blue-700"
}`}
disabled={disabled}
onClick={handleInsert}
>
Insert
</Button>
<Button
className={`px-4 py-2 font-bold rounded-md hover:bg-gray-200 text-sm ${
disabled ? "cursor-not-allowed bg-gray-200" : "bg-gray-300"
}`}
textColor={disabled ? "text-gray-400" : "text-slate-800"}
disabled={disabled}
onClick={handleTryAgain}
>
Try again
</Button>
<Button
className={`px-4 py-2 font-bold rounded-md hover:bg-gray-200 text-sm ${
abortBtnDisable
? "cursor-not-allowed bg-gray-200"
: "bg-gray-300"
}`}
textColor={abortBtnDisable ? "text-gray-400" : "text-slate-800"}
onClick={handleAbort}
disabled={abortBtnDisable}
>
Stop
</Button>
</div>
</>
)}
</div>
<form className="w-full flex gap-2" onSubmit={handleSubmit(submit)}>
<Input
placeholder="Ask AI to edit or generate"
{...register("prompt", {
required: { value: true },
minLength: { value: 4 },
})}
className={`focus:border-blue-500 ${
disabled && "cursor-not-allowed"
}`}
disabled={disabled}
/>
<Button
className={`w-10 rounded-md text-2xl ${
disabled && "cursor-not-allowed"
}`}
type="submit"
disabled={isSubmitting || disabled}
>
➤
</Button>
</form>
</div>
);
};
export default AiHelper;
Step 3: Add the custom plugin icon and action
<Editor
apiKey={conf.rteapikey}
onInit={handleEditorInit}
value={value || defaultValue}
init={{
height: 500,
menubar: true,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar:
"undo redo | blocks | " +
"AI | bold italic forecolor | alignleft aligncenter " +
"alignright alignjustify | bullist numlist outdent indent | " +
"removeformat | help",
content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
}}
/>
const handleEditorInit = (evt, editor) => {
editor.ui.registry.addButton('AI', {
icon: 'AI',
tooltip: 'Ask AI',
onAction: () => setIsActive(true)
});
};
Yeah the icon is now added to the toolbar, now when the user clicks on it the setIsActive will be true and the prompt box will appear
{isActive && <AiHelper editorInstance={editorInstance} />}
Step 4: Setup the socket.io :
npm install socket.io
import { io } from "socket.io-client"
import conf from "../conf/conf";
// Initialize the socket without connecting
const socket = io('http://localhost:3000', {
autoConnect: false, // Prevent automatic connection
});
// Function to connect with the query parameter
function connectWithUserId(userId) {
socket.io.opts.query = { userId }; // Set the query parameters dynamically
socket.connect(); // Manually initiate the connection
}
export {socket, connectWithUserId}
Step 5: Setup the server and install the dependencies :
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2",
"groq-sdk": "^0.9.1",
"socket.io": "^4.8.1"
},
Setup the socket server:
import express from 'express'
import { createServer } from 'http'
import { Server } from 'socket.io'
import getGroqChatCompletion from './getGroqChatCompletion.js'
import dotenv from 'dotenv'
const app = express()
const server = createServer(app)
const io = new Server(server, {
cors: {origin: process.env.CORS_ORIGIN}
})
dotenv.config({
path: './.env'
})
const PORT = process.env.PORT || 8000
server.listen(PORT, () => {
console.log(`Socket.IO server running on http://localhost:${PORT}`);
});
io.on("connection", (socket) => {
const userId = JSON.stringify(socket.handshake.query)
const userRoom = `user:${userId}`
socket.join(userRoom);
console.log(`User connected: ${userId}`);
let abortFlag = false
socket.on("abort", (data) => {
abortFlag = data
})
socket.on("prompt", async (prompt) => {
if (prompt) {
abortFlag = false
const suggestion = await getGroqChatCompletion(prompt);
for await (const chunk of suggestion) {
if (abortFlag) break;
if (chunk && chunk.choices && chunk.choices[0].delta) {
const text = chunk.choices[0].delta;
if (text.content) {
io.to(userRoom).emit("suggestion", text.content);
}
} else {
console.error("Unexpected chunk format:", chunk);
}
}
io.to(userRoom).emit("complete", true);
}
});
});
Now set the Groq SDK to get the suggestions :
import Groq from "groq-sdk";
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY });
export default async function getGroqChatCompletion(prompt) {
try {
const response = await groq.chat.completions.create({
messages: [
{
"role": "system",
"content": "if the user prompt is related to blog or writing articles on any topic write it professionally with proper headings and pointers like a actual writer"
},
{
role: "user",
content: prompt,
},
],
model: "llama-3.3-70b-versatile",
max_tokens: 1024,
stream: true,
});
const reader = response.iterator();
return reader
} catch (error) {
console.log("ERROR:: getGroqChatCompletion :: ", error);
return "FAILED TO GENERATE"
}
}
So your custom plugin is ready now you can generate suggestions from AI just like the paid plugin does
Here’s my blogging platform project where I have implemented this feature