2024-10-30 16:01:24 +05:30
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { exampleSetup } from 'prosemirror-example-setup';
|
2024-11-01 15:31:54 +05:30
|
|
|
import { inputRules } from 'prosemirror-inputrules';
|
2024-10-30 16:01:24 +05:30
|
|
|
import { EditorState } from 'prosemirror-state';
|
2024-11-01 15:31:54 +05:30
|
|
|
import { EditorView } from 'prosemirror-view';
|
2024-11-01 14:13:17 +05:30
|
|
|
import React, { memo, useEffect, useRef } from 'react';
|
2024-10-30 16:01:24 +05:30
|
|
|
|
2024-11-15 10:13:21 -05:00
|
|
|
import { Suggestion } from '@/lib/db/schema';
|
2024-10-30 16:01:24 +05:30
|
|
|
import {
|
2024-11-01 15:31:54 +05:30
|
|
|
documentSchema,
|
|
|
|
|
handleTransaction,
|
|
|
|
|
headingRule,
|
|
|
|
|
} from '@/lib/editor/config';
|
|
|
|
|
import {
|
|
|
|
|
buildContentFromDocument,
|
|
|
|
|
buildDocumentFromContent,
|
|
|
|
|
createDecorations,
|
|
|
|
|
} from '@/lib/editor/functions';
|
|
|
|
|
import {
|
|
|
|
|
projectWithPositions,
|
2024-10-30 16:01:24 +05:30
|
|
|
suggestionsPlugin,
|
|
|
|
|
suggestionsPluginKey,
|
|
|
|
|
} from '@/lib/editor/suggestions';
|
|
|
|
|
|
|
|
|
|
type EditorProps = {
|
|
|
|
|
content: string;
|
2024-11-01 15:31:54 +05:30
|
|
|
saveContent: (updatedContent: string, debounce: boolean) => void;
|
2024-10-30 16:01:24 +05:30
|
|
|
status: 'streaming' | 'idle';
|
2024-11-01 15:31:54 +05:30
|
|
|
isCurrentVersion: boolean;
|
2024-10-30 16:01:24 +05:30
|
|
|
currentVersionIndex: number;
|
|
|
|
|
suggestions: Array<Suggestion>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function PureEditor({
|
|
|
|
|
content,
|
2024-11-01 15:31:54 +05:30
|
|
|
saveContent,
|
|
|
|
|
suggestions,
|
|
|
|
|
status,
|
2024-10-30 16:01:24 +05:30
|
|
|
}: EditorProps) {
|
2024-11-01 15:31:54 +05:30
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const editorRef = useRef<EditorView | null>(null);
|
2024-10-30 16:01:24 +05:30
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-11-01 15:31:54 +05:30
|
|
|
if (containerRef.current && !editorRef.current) {
|
|
|
|
|
const state = EditorState.create({
|
|
|
|
|
doc: buildDocumentFromContent(content),
|
|
|
|
|
plugins: [
|
|
|
|
|
...exampleSetup({ schema: documentSchema, menuBar: false }),
|
|
|
|
|
inputRules({
|
|
|
|
|
rules: [
|
|
|
|
|
headingRule(1),
|
|
|
|
|
headingRule(2),
|
|
|
|
|
headingRule(3),
|
|
|
|
|
headingRule(4),
|
|
|
|
|
headingRule(5),
|
|
|
|
|
headingRule(6),
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
suggestionsPlugin,
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
editorRef.current = new EditorView(containerRef.current, {
|
|
|
|
|
state,
|
|
|
|
|
});
|
2024-10-30 16:01:24 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
2024-11-01 15:31:54 +05:30
|
|
|
if (editorRef.current) {
|
|
|
|
|
editorRef.current.destroy();
|
|
|
|
|
editorRef.current = null;
|
2024-10-30 16:01:24 +05:30
|
|
|
}
|
|
|
|
|
};
|
2024-11-01 15:31:54 +05:30
|
|
|
// NOTE: we only want to run this effect once
|
|
|
|
|
// eslint-disable-next-line
|
|
|
|
|
}, []);
|
2024-10-30 16:01:24 +05:30
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-11-01 15:31:54 +05:30
|
|
|
if (editorRef.current) {
|
|
|
|
|
editorRef.current.setProps({
|
|
|
|
|
dispatchTransaction: (transaction) => {
|
|
|
|
|
handleTransaction({ transaction, editorRef, saveContent });
|
|
|
|
|
},
|
|
|
|
|
});
|
2024-10-30 16:01:24 +05:30
|
|
|
}
|
2024-11-01 15:31:54 +05:30
|
|
|
}, [saveContent]);
|
2024-10-30 16:01:24 +05:30
|
|
|
|
2024-11-01 15:31:54 +05:30
|
|
|
useEffect(() => {
|
|
|
|
|
if (editorRef.current && content) {
|
|
|
|
|
const currentContent = buildContentFromDocument(
|
|
|
|
|
editorRef.current.state.doc
|
2024-10-30 16:01:24 +05:30
|
|
|
);
|
|
|
|
|
|
2024-11-01 15:31:54 +05:30
|
|
|
if (status === 'streaming') {
|
|
|
|
|
const newDocument = buildDocumentFromContent(content);
|
2024-10-30 16:01:24 +05:30
|
|
|
|
2024-11-01 15:31:54 +05:30
|
|
|
const transaction = editorRef.current.state.tr.replaceWith(
|
|
|
|
|
0,
|
|
|
|
|
editorRef.current.state.doc.content.size,
|
|
|
|
|
newDocument.content
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
transaction.setMeta('no-save', true);
|
|
|
|
|
editorRef.current.dispatch(transaction);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-10-30 16:01:24 +05:30
|
|
|
|
2024-11-01 15:31:54 +05:30
|
|
|
if (currentContent !== content) {
|
|
|
|
|
const newDocument = buildDocumentFromContent(content);
|
2024-10-30 16:01:24 +05:30
|
|
|
|
2024-11-01 15:31:54 +05:30
|
|
|
const transaction = editorRef.current.state.tr.replaceWith(
|
|
|
|
|
0,
|
|
|
|
|
editorRef.current.state.doc.content.size,
|
|
|
|
|
newDocument.content
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
transaction.setMeta('no-save', true);
|
|
|
|
|
editorRef.current.dispatch(transaction);
|
2024-10-30 16:01:24 +05:30
|
|
|
}
|
2024-11-01 15:31:54 +05:30
|
|
|
}
|
|
|
|
|
}, [content, status]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (editorRef.current && editorRef.current.state.doc && content) {
|
|
|
|
|
const projectedSuggestions = projectWithPositions(
|
|
|
|
|
editorRef.current.state.doc,
|
|
|
|
|
suggestions
|
|
|
|
|
).filter(
|
|
|
|
|
(suggestion) => suggestion.selectionStart && suggestion.selectionEnd
|
|
|
|
|
);
|
2024-10-30 16:01:24 +05:30
|
|
|
|
2024-11-01 15:31:54 +05:30
|
|
|
const decorations = createDecorations(
|
|
|
|
|
projectedSuggestions,
|
|
|
|
|
editorRef.current
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const transaction = editorRef.current.state.tr;
|
|
|
|
|
transaction.setMeta(suggestionsPluginKey, { decorations });
|
|
|
|
|
editorRef.current.dispatch(transaction);
|
|
|
|
|
}
|
|
|
|
|
}, [suggestions, content]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="relative prose dark:prose-invert" ref={containerRef} />
|
|
|
|
|
);
|
2024-10-30 16:01:24 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function areEqual(prevProps: EditorProps, nextProps: EditorProps) {
|
|
|
|
|
if (prevProps.suggestions !== nextProps.suggestions) {
|
|
|
|
|
return false;
|
|
|
|
|
} else if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex) {
|
|
|
|
|
return false;
|
2024-11-01 15:31:54 +05:30
|
|
|
} else if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) {
|
|
|
|
|
return false;
|
|
|
|
|
} else if (
|
|
|
|
|
prevProps.status === 'streaming' &&
|
|
|
|
|
nextProps.status === 'streaming'
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
} else if (prevProps.content !== nextProps.content) {
|
|
|
|
|
return false;
|
|
|
|
|
} else if (prevProps.saveContent !== nextProps.saveContent) {
|
2024-10-30 16:01:24 +05:30
|
|
|
return false;
|
|
|
|
|
}
|
2024-11-01 15:31:54 +05:30
|
|
|
|
|
|
|
|
return true;
|
2024-10-30 16:01:24 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const Editor = memo(PureEditor, areEqual);
|