This repository got archived as the packages inside got migrated to a new monorepo: https://github.com/emergence-engineering/emergence-tools
- AI-powered grammar and style corrections for ProseMirror
- AI text completion, rewriting, translation, tone changes and more — with streaming
- Inline autocomplete with ghost text (like GitHub Copilot)
- Paragraph-level processing with dirty state tracking and parallel execution
- Multiple AI model support with automatic fallback
- Create an account on SuggestCat and generate an API key
- Install the package
npm i prosemirror-suggestcat-plugin- Add the plugins you need to your ProseMirror (or TipTap) editor using your API key
- Import the styles or write your own CSS
- Track your usage and manage API keys on your admin dashboard
Checks your text for grammar and style issues paragraph by paragraph. Only edited paragraphs are re-checked, and multiple paragraphs are processed in parallel.
import {
grammarSuggestPluginV2,
grammarSuggestV2Key,
ActionType,
} from "prosemirror-suggestcat-plugin";
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
doc: schema.nodeFromJSON(initialDoc),
plugins: [
...exampleSetup({ schema }),
grammarSuggestPluginV2("<YOUR_API_KEY>", {
debounceMs: 1000,
batchSize: 4,
}),
],
}),
});
// Initialize the grammar checker
view.dispatch(
view.state.tr.setMeta(grammarSuggestV2Key, {
type: ActionType.INIT,
metadata: {},
}),
);interface GrammarSuggestV2Options {
apiKey: string;
apiEndpoint?: string;
model?: string | AIModel;
fallback?: {
fallbackModel: string | AIModel;
failureThreshold?: number; // default: 3
};
batchSize?: number; // parallel workers, default: 4
maxRetries?: number; // default: 3
backoffBase?: number; // default: 2000ms
debounceMs?: number; // default: 1000ms
createPopup?: (
view: EditorView,
decoration: Decoration,
pos: number,
applySuggestion: () => void,
discardSuggestion: () => void,
requestHint: () => Promise<string>,
) => HTMLElement;
}import "prosemirror-suggestcat-plugin/dist/styles/styles.css";Or style the decorations yourself using these CSS classes:
.grammarSuggestionV2— inline decoration on suggestions.removalSuggestionV2— when the suggestion is a deletion.grammarSuggestionV2-selected— currently selected suggestion.grammarPopupV2— popup container
AI text completion and transformation with streaming support. Use it to complete, shorten, lengthen, simplify, explain, translate text and more.
import { completePluginV2 } from "prosemirror-suggestcat-plugin";
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
doc: schema.nodeFromJSON(initialDoc),
plugins: [
...exampleSetup({ schema }),
completePluginV2("<YOUR_API_KEY>", {
maxSelection: 1000,
}),
],
}),
});Use action functions instead of dispatching metas directly:
import {
startTask,
acceptResult,
rejectResult,
cancelTask,
getCompleteState,
AiPromptsWithoutParam,
AiPromptsWithParam,
MoodParamType,
TranslationTargetLanguage,
} from "prosemirror-suggestcat-plugin";
// Continue writing from cursor
startTask(view, AiPromptsWithoutParam.Complete);
// Transform selected text
startTask(view, AiPromptsWithoutParam.MakeShorter);
startTask(view, AiPromptsWithoutParam.MakeLonger);
startTask(view, AiPromptsWithoutParam.Simplify);
startTask(view, AiPromptsWithoutParam.Explain);
startTask(view, AiPromptsWithoutParam.ActionItems);
startTask(view, AiPromptsWithoutParam.Improve);
// Tasks with parameters
startTask(view, AiPromptsWithParam.ChangeTone, {
mood: MoodParamType.Friendly,
});
startTask(view, AiPromptsWithParam.Translate, {
targetLanguage: TranslationTargetLanguage.Spanish,
});
// Accept or reject the result once streaming finishes
acceptResult(view);
rejectResult(view);
// Cancel an in-progress task
cancelTask(view);IDLE -> PENDING -> STREAMING -> PREVIEW -> APPLYING -> IDLE
During STREAMING the result is built up incrementally. At PREVIEW you can accept or reject. Only one task runs at a time.
Inline ghost-text completions that appear after the cursor as you type. Press Tab to accept, Escape to dismiss.
import { autoCompletePlugin } from "prosemirror-suggestcat-plugin";
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
doc: schema.nodeFromJSON(initialDoc),
plugins: [
...exampleSetup({ schema }),
autoCompletePlugin("<YOUR_API_KEY>", {
debounceMs: 500,
maxContextLength: 2000,
}),
],
}),
});interface AutoCompleteOptions {
debounceMs: number; // default: 500
maxContextLength: number; // default: 2000
apiEndpoint?: string;
model?: string;
ghostTextClass?: string; // default: "autoCompleteGhostText"
}Add CSS for the ghost text:
.autoCompleteGhostText {
color: #9ca3af;
opacity: 0.7;
pointer-events: none;
}import {
setAutoCompleteEnabled,
acceptAutoCompletion,
dismissAutoCompletion,
isAutoCompleteEnabled,
hasAutoCompletion,
} from "prosemirror-suggestcat-plugin";
setAutoCompleteEnabled(view, true);
setAutoCompleteEnabled(view, false);
if (hasAutoCompletion(view)) {
acceptAutoCompletion(view);
// or
dismissAutoCompletion(view);
}All plugins communicate with the SuggestCat backend through a shared API module. You can configure the endpoint and model per plugin, or use the defaults (openai:gpt-4o-mini).
The module exposes two request functions if you need to use them directly:
grammarRequest(options)— a non-streaming POST that returns the corrected text and a list of modificationsstreamingRequest(options, callbacks)— a streaming request withonChunk,onCompleteandonErrorcallbacks, used by the complete and autocomplete plugins. Supports cancellation viaAbortSignal.
Both accept an ApiConfig (apiKey, optional endpoint, optional model). You can use createApiConfig or createGrammarApiConfig to fill in defaults.
import {
streamingRequest,
grammarRequest,
createApiConfig,
createGrammarApiConfig,
} from "prosemirror-suggestcat-plugin";The grammar suggestion plugin is built on top of a generic block runner framework. Instead of sending the entire document to the API, it splits the document into processing units (paragraphs by default) and processes them in parallel.
Key properties:
- Paragraph-level processing — each paragraph is an independent unit, so large documents don't result in oversized API calls
- Dirty state tracking — when a paragraph is edited, only that unit is marked dirty and re-processed after a debounce delay. The plugin skips dirty marking for its own document changes (e.g. applying a suggestion)
- Parallel execution — multiple units are processed concurrently, controlled by
batchSize. A unit goes throughQUEUED → PROCESSING → DONE(orBACKOFF → retryon failure) - Retry with backoff — failed units are retried up to
maxRetriestimes with exponential backoff
The block runner is also exported as a standalone building block. You can use it to build your own paragraph-level processing plugins. The package includes a few example plugins (link detector, word complexity, sentence length) that demonstrate how to wire up a custom processor.
import {
blockRunnerPlugin,
createBlockRunnerKey,
ActionType,
dispatchAction,
} from "prosemirror-suggestcat-plugin";To create a block runner plugin, provide a pluginKey, a unitProcessor (async function that receives a processing unit and returns a result), a decorationFactory (turns results into ProseMirror decorations), and optionally a widgetFactory (shows per-unit status indicators) and a decorationTransformer (filters/modifies decorations based on context state).
The block runner exports utility functions you can use in your processor or elsewhere:
extractTextWithMapping(doc, from, to)— extracts text from a document range and builds a position mapping between text offsets and doc positionstextToDocPos(textPos, mapping)— converts a text-space position back to a document position using the mappinggetUnitsInRange(doc, from, to, nodeTypes?)— finds all matching nodes in a rangecreateUnitsFromDocument(doc, from, to, metadataFactory, nodeTypes?)— creates processing units from document nodesgetProgress(state)— returns{ completed, total, decorations }for progress trackingcalculateBackoff(retryCount, baseMs)— computes the backoff delay for retriesallUnitsFinished(units)— checks if all units are in a terminal state (DONE or ERROR)
You can control the block runner by dispatching actions via dispatchAction(view, pluginKey, action):
import { dispatchAction, ActionType } from "prosemirror-suggestcat-plugin";
// Initialize — creates units from the document and starts processing
dispatchAction(view, myPluginKey, {
type: ActionType.INIT,
metadata: {},
});
// Pause processing
dispatchAction(view, myPluginKey, { type: ActionType.FINISH });
// Resume paused processing
dispatchAction(view, myPluginKey, { type: ActionType.RESUME });
// Clear all state
dispatchAction(view, myPluginKey, { type: ActionType.CLEAR });
// Remove a specific decoration
dispatchAction(view, myPluginKey, {
type: ActionType.REMOVE_DECORATION,
id: decorationId,
});
// Update context state (e.g. for filtering or selection)
dispatchAction(view, myPluginKey, {
type: ActionType.UPDATE_CONTEXT,
contextState: { selectedSuggestionId: someId },
});There are also convenience wrappers: pauseRunner(view, pluginKey), resumeRunner(view, pluginKey), and canResume(state).
All plugins accept a model option:
type AIModel =
| "openai:gpt-4o"
| "openai:gpt-4o-mini" // default
| "cerebras:llama-3.1-8b"
| "cerebras:llama-3.3-70b"
| "cerebras:qwen-3-32b";All plugins work with TipTap by wrapping them in an extension:
import { Extension } from "@tiptap/core";
import {
grammarSuggestPluginV2,
grammarSuggestV2Key,
ActionType,
completePluginV2,
autoCompletePlugin,
} from "prosemirror-suggestcat-plugin";
const SuggestCatExtension = Extension.create({
name: "suggestcat",
addProseMirrorPlugins() {
return [
grammarSuggestPluginV2("<YOUR_API_KEY>"),
completePluginV2("<YOUR_API_KEY>"),
autoCompletePlugin("<YOUR_API_KEY>"),
];
},
});For a ready-made React UI with a slash menu, suggestion overlay and "Ask AI" tooltip, see prosemirror-suggestcat-plugin-react.
