Document-Wide Edits with Plate.js + Morph
Use Morph Apply for lightning-fast document-wide transformations in Plate.js rich text editors
This guide is a work in progress.
Document-Wide Edits with Plate.js + Morph
While Plate.js has excellent built-in AI for local content generation and improvements, Morph Apply excels at fast, document-wide transformations. Use Morph when you need to restructure, reformat, or completely transform entire documents at 2000+ tokens/second.
What Morph Does Best for Rich Text
Document Restructuring: Reorganize entire documents with new headings, sections, and flow Style Transformations: Convert writing style across entire documents (formal ↔ casual, technical ↔ accessible) Format Conversions: Transform document types (blog post → technical spec, notes → article) Bulk Content Edits: Apply consistent changes across all sections simultaneously
Quick Setup
npm install @udecode/plate-ai @udecode/plate-markdown
Basic Document-Wide Editor
'use client';
import { Plate, PlateContent, createPlateEditor } from '@udecode/plate-common/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
import { basicNodesPlugins } from '@udecode/plate-basic-nodes';
import { useState } from 'react';
export function DocumentTransformEditor() {
const [isTransforming, setIsTransforming] = useState(false);
const editor = createPlateEditor({
plugins: [
...basicNodesPlugins,
MarkdownPlugin,
],
});
const transformDocument = async (transformation: string) => {
setIsTransforming(true);
try {
// Get entire document as markdown
const fullDocument = editor.api.markdown.serialize();
// Send to Morph for document-wide transformation
const response = await fetch('/api/morph/transform', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
document: fullDocument,
transformation
}),
});
const transformedMarkdown = await response.text();
// Replace entire document with transformed content
const newNodes = editor.api.markdown.deserialize(transformedMarkdown);
editor.children = newNodes;
editor.onChange();
} catch (error) {
console.error('Document transformation failed:', error);
} finally {
setIsTransforming(false);
}
};
return (
<div className="h-screen flex">
{/* Document Editor */}
<div className="flex-1 flex flex-col">
<Plate editor={editor}>
<PlateContent
className="flex-1 p-6 focus:outline-none"
placeholder="Write your document here..."
/>
</Plate>
</div>
{/* Transform Panel */}
<div className="w-80 border-l bg-gray-50 p-4">
<h3 className="font-semibold mb-4">Document Transformations</h3>
<div className="space-y-2">
<TransformButton
onClick={() => transformDocument('Restructure this document with clear headings, subheadings, and logical flow')}
disabled={isTransforming}
label="Restructure Document"
/>
<TransformButton
onClick={() => transformDocument('Convert this to a formal, professional tone throughout')}
disabled={isTransforming}
label="Make Formal"
/>
<TransformButton
onClick={() => transformDocument('Convert this to a casual, conversational tone throughout')}
disabled={isTransforming}
label="Make Casual"
/>
<TransformButton
onClick={() => transformDocument('Transform this into a technical specification with clear sections and requirements')}
disabled={isTransforming}
label="→ Tech Spec"
/>
<TransformButton
onClick={() => transformDocument('Convert this into a blog post with engaging headings and examples')}
disabled={isTransforming}
label="→ Blog Post"
/>
<TransformButton
onClick={() => transformDocument('Add consistent formatting, improve clarity, and fix any structural issues throughout')}
disabled={isTransforming}
label="Polish & Format"
/>
</div>
{isTransforming && (
<div className="mt-4 p-3 bg-blue-100 rounded-lg">
<div className="flex items-center text-blue-700">
<div className="animate-spin h-4 w-4 border-2 border-blue-600 border-t-transparent rounded-full mr-2"></div>
Transforming document...
</div>
</div>
)}
</div>
</div>
);
}
function TransformButton({ onClick, disabled, label }: {
onClick: () => void;
disabled: boolean;
label: string;
}) {
return (
<button
onClick={onClick}
disabled={disabled}
className="w-full text-left px-3 py-2 bg-white border rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
{label}
</button>
);
}
Streaming Document Transforms
For large documents, stream the transformation in real-time:
'use client';
import { useState } from 'react';
import { Plate, PlateContent } from '@udecode/plate-common/react';
export function StreamingTransformEditor() {
const [isStreaming, setIsStreaming] = useState(false);
const [streamingContent, setStreamingContent] = useState('');
const streamTransform = async (transformation: string) => {
setIsStreaming(true);
setStreamingContent('');
const fullDocument = editor.api.markdown.serialize();
const response = await fetch('/api/morph/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ document: fullDocument, transformation }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let accumulated = '';
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
accumulated += chunk;
setStreamingContent(accumulated);
}
// Apply final transformed content
const newNodes = editor.api.markdown.deserialize(accumulated);
editor.children = newNodes;
editor.onChange();
setIsStreaming(false);
setStreamingContent('');
};
return (
<div className="grid grid-cols-2 gap-4 h-screen p-4">
<div className="flex flex-col">
<h3 className="text-lg font-semibold mb-2">Original Document</h3>
<Plate editor={editor}>
<PlateContent className="flex-1 p-4 border rounded-lg" />
</Plate>
</div>
<div className="flex flex-col">
<h3 className="text-lg font-semibold mb-2">
Live Transformation
{isStreaming && <span className="text-blue-600 ml-2">● Streaming...</span>}
</h3>
<div className="flex-1 p-4 border rounded-lg bg-gray-50 overflow-auto">
<pre className="whitespace-pre-wrap font-sans">
{streamingContent || 'Transformed document will appear here...'}
</pre>
</div>
</div>
</div>
);
}
Advanced Document Operations
Custom Transformation Templates
export const DOCUMENT_TRANSFORMS = {
restructure: {
name: 'Restructure',
prompt: 'Reorganize this document with clear headings, logical flow, and proper section hierarchy',
},
summarize: {
name: 'Create Executive Summary',
prompt: 'Create a comprehensive executive summary at the top, then restructure the content with key insights highlighted',
},
expand: {
name: 'Expand with Details',
prompt: 'Expand each section with more detailed explanations, examples, and supporting information',
},
simplify: {
name: 'Simplify Language',
prompt: 'Rewrite the entire document using simpler language while maintaining all key information',
},
academic: {
name: 'Academic Style',
prompt: 'Transform into academic writing style with proper citations, formal tone, and structured arguments',
},
business: {
name: 'Business Proposal',
prompt: 'Convert into a business proposal format with executive summary, problem statement, solution, and next steps',
},
tutorial: {
name: 'Step-by-Step Tutorial',
prompt: 'Restructure as a step-by-step tutorial with clear instructions, prerequisites, and examples',
},
};
export function useDocumentTransforms(editor: PlateEditor) {
const [isTransforming, setIsTransforming] = useState(false);
const transform = async (transformKey: keyof typeof DOCUMENT_TRANSFORMS) => {
setIsTransforming(true);
const transform = DOCUMENT_TRANSFORMS[transformKey];
const fullDocument = editor.api.markdown.serialize();
try {
const response = await fetch('/api/morph/transform', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
document: fullDocument,
transformation: transform.prompt,
}),
});
const transformedContent = await response.text();
const newNodes = editor.api.markdown.deserialize(transformedContent);
editor.children = newNodes;
editor.onChange();
} finally {
setIsTransforming(false);
}
};
return { transform, isTransforming, transforms: DOCUMENT_TRANSFORMS };
}
Document Analysis & Transformation
'use client';
import { useState } from 'react';
import { useDocumentTransforms } from '@/hooks/useDocumentTransforms';
export function DocumentAnalyzer({ editor }: { editor: PlateEditor }) {
const [analysis, setAnalysis] = useState<string | null>(null);
const { transform, isTransforming, transforms } = useDocumentTransforms(editor);
const analyzeDocument = async () => {
const fullDocument = editor.api.markdown.serialize();
const response = await fetch('/api/morph/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
document: fullDocument,
analysis: 'Analyze this document structure, identify areas for improvement, and suggest specific transformations',
}),
});
const analysisResult = await response.text();
setAnalysis(analysisResult);
};
return (
<div className="space-y-4">
<div>
<button
onClick={analyzeDocument}
className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
Analyze Document
</button>
</div>
{analysis && (
<div className="p-4 bg-gray-50 rounded-lg">
<h4 className="font-semibold mb-2">Document Analysis</h4>
<div className="text-sm text-gray-700 whitespace-pre-wrap">
{analysis}
</div>
</div>
)}
<div className="space-y-2">
<h4 className="font-semibold">Quick Transformations</h4>
{Object.entries(transforms).map(([key, transform]) => (
<button
key={key}
onClick={() => transform(key as keyof typeof transforms)}
disabled={isTransforming}
className="w-full text-left px-3 py-2 bg-white border rounded-lg hover:bg-gray-50 disabled:opacity-50"
>
{transform.name}
</button>
))}
</div>
</div>
);
}
Batch Document Processing
For processing multiple documents or sections:
'use client';
interface DocumentSection {
id: string;
title: string;
content: string;
}
export function BatchDocumentProcessor() {
const [sections, setSections] = useState<DocumentSection[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const processSections = async (transformation: string) => {
setIsProcessing(true);
try {
const results = await Promise.all(
sections.map(async (section) => {
const response = await fetch('/api/morph/transform', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
document: section.content,
transformation: `${transformation}\n\nFocus on this section: "${section.title}"`,
}),
});
const transformed = await response.text();
return { ...section, content: transformed };
})
);
setSections(results);
} finally {
setIsProcessing(false);
}
};
return (
<div className="space-y-4">
<div className="flex gap-2">
<button
onClick={() => processSections('Improve clarity and readability')}
disabled={isProcessing}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
Polish All Sections
</button>
<button
onClick={() => processSections('Make more technical and detailed')}
disabled={isProcessing}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
Make Technical
</button>
</div>
{isProcessing && (
<div className="text-blue-600">
Processing {sections.length} sections...
</div>
)}
<div className="space-y-4">
{sections.map((section) => (
<div key={section.id} className="border rounded-lg p-4">
<h3 className="font-semibold mb-2">{section.title}</h3>
<div className="text-sm text-gray-700">{section.content}</div>
</div>
))}
</div>
</div>
);
}
Production Considerations
Document Backup & History
export function useDocumentHistory(editor: PlateEditor) {
const [history, setHistory] = useState<Array<{
timestamp: Date;
content: string;
transformation: string;
}>>([]);
const saveSnapshot = (transformation: string) => {
const content = editor.api.markdown.serialize();
setHistory(prev => [...prev, {
timestamp: new Date(),
content,
transformation,
}]);
};
const restoreSnapshot = (index: number) => {
const snapshot = history[index];
if (snapshot) {
const nodes = editor.api.markdown.deserialize(snapshot.content);
editor.children = nodes;
editor.onChange();
}
};
return { history, saveSnapshot, restoreSnapshot };
}
Performance for Large Documents
export function chunkDocument(content: string, maxChunkSize: number = 4000): string[] {
const sections = content.split(/\n#{1,3}\s/); // Split on headers
const chunks: string[] = [];
let currentChunk = '';
for (const section of sections) {
if (currentChunk.length + section.length > maxChunkSize) {
if (currentChunk) chunks.push(currentChunk);
currentChunk = section;
} else {
currentChunk += (currentChunk ? '\n# ' : '') + section;
}
}
if (currentChunk) chunks.push(currentChunk);
return chunks;
}
export async function transformLargeDocument(
document: string,
transformation: string
): Promise<string> {
const chunks = chunkDocument(document);
const transformedChunks = await Promise.all(
chunks.map(chunk => transformChunk(chunk, transformation))
);
return transformedChunks.join('\n\n');
}
Key Benefits
Speed: Transform entire documents in seconds, not minutes Consistency: Apply changes uniformly across all content Scope: Handle document-wide restructuring that’s impossible with local AI Efficiency: Process large documents that would overwhelm other approaches
This approach complements Plate.js’s built-in AI perfectly - use Plate’s AI for local improvements and content generation, use Morph for fast document-wide transformations.
For best results, combine both approaches: use Plate’s AI for writing assistance and Morph for document-level transformations and restructuring.
- Document-Wide Edits with Plate.js + Morph
- What Morph Does Best for Rich Text
- Quick Setup
- Basic Document-Wide Editor
- Streaming Document Transforms
- Advanced Document Operations
- Custom Transformation Templates
- Document Analysis & Transformation
- Batch Document Processing
- Production Considerations
- Document Backup & History
- Performance for Large Documents
- Key Benefits