Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions artifacts/sample-rich.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><body><h1>Rich Test</h1><p>This is a <strong>rich</strong> document.</p></body></html>
1 change: 1 addition & 0 deletions components/FolderOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const DOC_TYPE_LABELS: Record<DocType, string> = {
source_code: 'Source code',
pdf: 'PDFs',
image: 'Images',
rich_text: 'Rich documents',
};

const formatDocTypeLabel = (docType: DocType) => DOC_TYPE_LABELS[docType] ?? docType.replace(/_/g, ' ');
Expand Down
115 changes: 95 additions & 20 deletions components/PromptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import IconButton from './IconButton';
import Button from './Button';
import MonacoEditor, { CodeEditorHandle } from './CodeEditor';
import MonacoDiffEditor from './MonacoDiffEditor';
import RichTextEditor, { type RichTextEditorHandle } from './RichTextEditor';
import PreviewPane from './PreviewPane';
import LanguageDropdown from './LanguageDropdown';
import PythonExecutionPanel from './PythonExecutionPanel';
Expand Down Expand Up @@ -42,6 +43,12 @@ interface DocumentEditorProps {
commandTriggers: DocumentCommandTriggers;
}

type EditorBridgeHandle = {
format: () => void;
setScrollTop: (scrollTop: number) => void;
getScrollInfo: () => Promise<{ scrollTop: number; scrollHeight: number; clientHeight: number }>;
};

const useCommandTrigger = (trigger: number, callback: () => void | Promise<void>) => {
const previousRef = useRef<number | null>(null);
const callbackRef = useRef(callback);
Expand Down Expand Up @@ -89,6 +96,8 @@ const PREVIEWABLE_LANGUAGES = new Set<string>([
'image/svg+xml',
]);

type EditorEngine = 'monaco' | 'plate';

const resolveDefaultViewMode = (mode: ViewMode | null | undefined, languageHint: string | null | undefined): ViewMode => {
if (mode) return mode;
const normalizedHint = languageHint?.toLowerCase();
Expand Down Expand Up @@ -151,6 +160,11 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
const [splitSize, setSplitSize] = useState(50);
const isLocked = Boolean(documentNode.locked);
const [isLocking, setIsLocking] = useState(false);
const editorEnginePreferencesRef = useRef<Record<string, EditorEngine>>({});
const defaultEngineForDocument: EditorEngine = documentNode.doc_type === 'rich_text' ? 'plate' : 'monaco';
const [editorEngine, setEditorEngine] = useState<EditorEngine>(
editorEnginePreferencesRef.current[documentNode.id] ?? defaultEngineForDocument,
);
const { addLog } = useLogger();
const { skipNextAutoSave } = useDocumentAutoSave({
documentId: documentNode.id,
Expand Down Expand Up @@ -197,7 +211,7 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
const titleInputRef = useRef<HTMLInputElement>(null);
const languageButtonRef = useRef<HTMLButtonElement | null>(null);
const isContentInitialized = useRef(false);
const editorRef = useRef<CodeEditorHandle>(null);
const editorRef = useRef<EditorBridgeHandle | null>(null);
const previewScrollRef = useRef<HTMLDivElement>(null);
const isSyncing = useRef(false);
const syncTimeout = useRef<number | null>(null);
Expand Down Expand Up @@ -253,6 +267,9 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
setIsDirty(false);
setIsSaving(false);
setIsDiffMode(false);
const storedEngine = editorEnginePreferencesRef.current[documentNode.id];
const resolvedEngine: EditorEngine = storedEngine ?? (documentNode.doc_type === 'rich_text' ? 'plate' : 'monaco');
setEditorEngine(resolvedEngine);
prevDocumentIdRef.current = documentNode.id;
prevDocumentContentRef.current = documentNode.content;
return;
Expand Down Expand Up @@ -301,6 +318,15 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
prevLockedRef.current = isLocked;
}, [isLocked, documentNode.content]);

useEffect(() => {
const stored = editorEnginePreferencesRef.current[documentNode.id];
if (stored) {
setEditorEngine(stored);
return;
}
setEditorEngine(documentNode.doc_type === 'rich_text' ? 'plate' : 'monaco');
}, [documentNode.doc_type, documentNode.id]);

useEffect(() => {
}, [content]);

Expand Down Expand Up @@ -476,6 +502,17 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
setViewMode(newMode);
onViewModeChange(newMode);
}, [onViewModeChange]);

const handleEditorEngineChange = useCallback((engine: EditorEngine) => {
if (editorEngine === engine) {
return;
}
editorEnginePreferencesRef.current[documentNode.id] = engine;
setEditorEngine(engine);
if (engine === 'plate') {
setIsDiffMode(false);
}
}, [documentNode.id, editorEngine]);

const handleFormatDocument = () => {
if (isLocked) {
Expand Down Expand Up @@ -659,10 +696,11 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({

const language = documentNode.language_hint || 'plaintext';
const normalizedLanguage = language.toLowerCase();
const supportsAiTools = ['markdown', 'plaintext'].includes(normalizedLanguage);
const isRichTextDocument = documentNode.doc_type === 'rich_text';
const supportsAiTools = ['markdown', 'plaintext', 'html'].includes(normalizedLanguage);
const canAddEmojiToTitle = documentNode.type === 'document';
const supportsPreview = PREVIEWABLE_LANGUAGES.has(normalizedLanguage);
const supportsFormatting = ['javascript', 'typescript', 'json', 'html', 'css', 'xml', 'yaml'].includes(normalizedLanguage);
const supportsFormatting = editorEngine === 'monaco' && ['javascript', 'typescript', 'json', 'html', 'css', 'xml', 'yaml'].includes(normalizedLanguage);
const scriptBridgeAvailable =
typeof window !== 'undefined' && (!!window.electronAPI || !!window.__DOCFORGE_SCRIPT_PREVIEW__);
const isPythonDocument = typeof window !== 'undefined' && !!window.electronAPI && (normalizedLanguage === 'python');
Expand Down Expand Up @@ -820,14 +858,15 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
}, [onZoomTargetChange]);

const renderContent = () => {
const showRichEditor = isRichTextDocument && editorEngine === 'plate' && !isDiffMode;
const editor = isDiffMode
? (
<MonacoDiffEditor
oldText={baselineContent}
newText={content}
language={language}
renderMode="inline"
readOnly={isLocked}
readOnly={isLocked || (isRichTextDocument && editorEngine === 'plate')}
onChange={isLocked ? undefined : setContent}
onScroll={handleEditorScroll}
fontFamily={settings.editorFontFamily}
Expand All @@ -837,22 +876,35 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
onFocusChange={handleEditorFocusChange}
/>
)
: (
<MonacoEditor
ref={editorRef}
content={content}
language={language}
onChange={setContent}
onScroll={handleEditorScroll}
customShortcuts={settings.customShortcuts}
fontFamily={settings.editorFontFamily}
fontSize={scaledEditorFontSize}
activeLineHighlightColorLight={settings.editorActiveLineHighlightColor}
activeLineHighlightColorDark={settings.editorActiveLineHighlightColorDark}
readOnly={isLocked}
onFocusChange={handleEditorFocusChange}
/>
);
: showRichEditor
? (
<RichTextEditor
ref={editorRef as unknown as React.Ref<RichTextEditorHandle>}
content={content}
onChange={setContent}
onScroll={handleEditorScroll}
readOnly={isLocked}
onFocusChange={handleEditorFocusChange}
fontFamily={settings.editorFontFamily}
fontSize={scaledEditorFontSize}
/>
)
: (
<MonacoEditor
ref={editorRef as unknown as React.Ref<CodeEditorHandle>}
content={content}
language={language}
onChange={setContent}
onScroll={handleEditorScroll}
customShortcuts={settings.customShortcuts}
fontFamily={settings.editorFontFamily}
fontSize={scaledEditorFontSize}
activeLineHighlightColorLight={settings.editorActiveLineHighlightColor}
activeLineHighlightColorDark={settings.editorActiveLineHighlightColorDark}
readOnly={isLocked}
onFocusChange={handleEditorFocusChange}
/>
);
const preview = (
<div
className="h-full w-full"
Expand Down Expand Up @@ -976,6 +1028,29 @@ const DocumentEditor: React.FC<DocumentEditorProps> = ({
<IconButton onClick={() => handleViewModeButton('split-horizontal')} tooltip="Split Horizontal" size="xs" className={`rounded-md ${viewMode === 'split-horizontal' ? 'bg-secondary text-primary' : ''}`}><LayoutHorizontalIcon className="w-4 h-4" /></IconButton>
</div>
)}
{isRichTextDocument && (
<>
<div className="h-5 w-px bg-border-color mx-1"></div>
<div className="flex items-center p-1 bg-background rounded-lg border border-border-color">
<IconButton
onClick={() => handleEditorEngineChange('plate')}
tooltip="Visual Editor"
size="xs"
className={`rounded-md px-2 ${editorEngine === 'plate' ? 'bg-secondary text-primary' : ''}`}
>
Visual
</IconButton>
<IconButton
onClick={() => handleEditorEngineChange('monaco')}
tooltip="Source Editor"
size="xs"
className={`rounded-md px-2 ${editorEngine === 'monaco' ? 'bg-secondary text-primary' : ''}`}
>
Source
</IconButton>
</div>
</>
)}
<div className="h-5 w-px bg-border-color mx-1"></div>
{supportsFormatting && (
<IconButton onClick={handleFormatDocument} tooltip="Format Document" size="xs" variant="ghost" disabled={isLocked || isLocking}>
Expand Down
Loading