Customize how changes are displayed
The AI Changes extension is designed with flexibility in mind. As a headless library, it gives you full control over how changes are displayed in your editor.
Default styles
By default, the AI Changes extension applies CSS classes to highlight modified content:
tiptap-ai-changes--old
for deleted texttiptap-ai-changes--new
for inserted text
The extension doesn't include any built-in styles, so you'll need to define your own CSS. Here's a complete example of basic styling for change highlighting:
:root {
--color-green-100: oklch(0.962 0.044 156.743);
--color-green-700: oklch(0.527 0.154 150.069);
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-700: oklch(0.505 0.213 27.518);
}
.tiptap-ai-changes--old,
.tiptap-ai-changes--old > * {
color: var(--color-red-700);
background-color: var(--color-red-100);
}
.tiptap-ai-changes--new,
.tiptap-ai-changes--new > * {
color: var(--color-green-700);
background-color: var(--color-green-100);
}
This will apply a red background to the deleted text, and a green background to the inserted text.
For more advanced styles, use the getCustomDecorations
configuration option.
Selected changes
A change is considered "selected" when the selection cursor is over it.
You can retrieve the selected change from the extension's storage object:
const storage = editor.extensionStorage.aiChanges
const selectedChange = storage.getSelectedChange()
To select a change programmatically, use the selectAiChange
command.
editor.commands.selectAiChange(changeId)
This will move the cursor to the beginning of the change, so that it is considered "selected".
The text of the selected change can be referenced in the getCustomDecorations
function to apply custom styles to it.
Customize the change's appearance
The getCustomDecorations
option allows you to control the appearance of changes and provide visual cues to the user.
It receives these arguments:
change
: The change object that contains the information about the change.changes
: A list of all tracked changes.isSelected
: A boolean that indicates if the change is selected. A change is selected when the cursor is on top of it.getDefaultDecorations
: A function that returns the default decorations for the change. If thegetCustomDecorations
function is not provided, the default decorations will be used.editor
: The Tiptap Editor instance.previousDoc
: The previous document before the AI made changes. The changes are obtained by comparing the current document with this one.currentDoc
: The document after the AI made changes.
AiChanges.configure({
getCustomDecorations({ change, isSelected, getDefaultDecorations }) {
// You can combine the default decorations of the AI Changes extension with your custom ones
const decorations = getDefaultDecorations()
// Add a custom element after the inserted text of the change
decorations.push(
Decoration.widget(change.newRange.to, () => {
const element = document.createElement('span')
element.textContent = '✅'
return element
}),
)
return decorations
},
})
The custom styles and elements are implemented with the Prosemirror Decorations API.
To learn how to show a popover when you select a change, follow this guide.
Show a popover when a change is selected
In most user review workflows, you'll need to display a popover or a tooltip over a selected change, with actions to accept or reject it.
To show a popover when you select a change, use the getCustomDecorations
option. It allows you to add custom elements to the changes, including popovers.
Below is a simplified example using React:
// First, define a hook to store the HTML element where the popover will be rendered
const [popoverElement, setPopoverElement] = useState<HTMLElement | null>(null)
AiChanges.configure({
getCustomDecorations({ change, isSelected, getDefaultDecorations }) {
const decorations = getDefaultDecorations()
// Then, create a Prosemirror decoration that contains the HTML element
// Add a custom element after the inserted text when the change is selected
if (isSelected) {
decorations.push(
Decoration.widget(change.newRange.to, () => {
const element = document.createElement('span')
setPopoverElement(element)
return element
}),
)
}
return decorations
},
})
const selectedChange = editor.extensionStorage.aiChanges.getSelectedChange()
if (popoverElement && selectedChange) {
// Then, add the HTML content to the custom element. In this example, we use React Portals to render the popover.
ReactDOM.createPortal(<Popover change={selectedChange} />, popoverElement)
}
We recommend using the Floating UI library to display the popover. You can see an example on how to do it in the demo.
Display changes in a sidebar outside the editor
You can access the changes data from the extension's storage object:
const storage = editor.extensionStorage.aiChanges
const changes = storage.getChanges()
Then, use this data to render a custom UI component. Here's an example using React:
// Get the changes from the Editor state.
const storage = editor.extensionStorage.aichange
const changes = storage.getchanges()
// Render the changes in the UI
return (
<div>
{changes.map((change) => (
<div key={change.id}>
<button onClick={() => editor.commands.acceptAiChange(change.id)}>Accept</button>
<button onClick={() => editor.commands.rejectAiChange(change.id)}>Reject</button>
</div>
))}
</div>
)
Hide and show changes
In some scenarios, you might want to continue tracking the changes but hide them in the UI. For example, when you are streaming AI-generated content. You can hide and show changes programmatically using the setShowAiChanges
command.
// Hide changes
editor.commands.setShowAiChanges(false)
// Show changes
editor.commands.setShowAiChanges(true)
This only affects the visual display of changes—the tracking mechanism continues to work in the background, and all changes remain accessible through the extension's storage methods.