diff --git a/surfsense_backend/app/routes/editor_routes.py b/surfsense_backend/app/routes/editor_routes.py
index 166164c50..79beebb66 100644
--- a/surfsense_backend/app/routes/editor_routes.py
+++ b/surfsense_backend/app/routes/editor_routes.py
@@ -38,7 +38,8 @@ logger = logging.getLogger(__name__)
router = APIRouter()
-EDITOR_PLATE_MAX_BYTES = 5 * 1024 * 1024
+EDITOR_PLATE_MAX_BYTES = 1 * 1024 * 1024
+EDITOR_PLATE_MAX_LINES = 5000
@router.get("/search-spaces/{search_space_id}/documents/{document_id}/editor-content")
@@ -83,16 +84,23 @@ async def get_editor_content(
def _build_response(md: str) -> dict:
size_bytes = len(md.encode("utf-8"))
- viewer_mode = "monaco" if size_bytes > EDITOR_PLATE_MAX_BYTES else "plate"
+ line_count = md.count("\n") + 1
+ too_large = (
+ size_bytes > EDITOR_PLATE_MAX_BYTES
+ or line_count > EDITOR_PLATE_MAX_LINES
+ )
+ viewer_mode = "monaco" if too_large else "plate"
return {
"document_id": document.id,
"title": document.title,
"document_type": document.document_type.value,
"source_markdown": md,
"content_size_bytes": size_bytes,
+ "line_count": line_count,
"chunk_count": chunk_count,
"viewer_mode": viewer_mode,
"editor_plate_max_bytes": EDITOR_PLATE_MAX_BYTES,
+ "editor_plate_max_lines": EDITOR_PLATE_MAX_LINES,
"updated_at": document.updated_at.isoformat()
if document.updated_at
else None,
diff --git a/surfsense_web/components/editor-panel/editor-panel.tsx b/surfsense_web/components/editor-panel/editor-panel.tsx
index 01983cbe1..3eadc51b2 100644
--- a/surfsense_web/components/editor-panel/editor-panel.tsx
+++ b/surfsense_web/components/editor-panel/editor-panel.tsx
@@ -17,6 +17,7 @@ import { toast } from "sonner";
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { DownloadOriginalButton } from "@/components/documents/download-original-button";
import { VersionHistoryButton } from "@/components/documents/version-history";
+import { PlateErrorBoundary } from "@/components/editor/plate-error-boundary";
import { SourceCodeEditor } from "@/components/editor/source-code-editor";
import {
fetchMemoryEditorDocument,
@@ -41,7 +42,8 @@ const PlateEditor = dynamic(
{ ssr: false, loading: () => }
);
-const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
+const LARGE_DOCUMENT_THRESHOLD = 1 * 1024 * 1024; // 1MB, matches backend
+const LARGE_DOCUMENT_LINE_THRESHOLD = 5000;
interface EditorContent {
document_id: number;
@@ -49,9 +51,11 @@ interface EditorContent {
document_type?: string;
source_markdown: string;
content_size_bytes?: number;
+ line_count?: number;
chunk_count?: number;
viewer_mode?: ViewerMode;
editor_plate_max_bytes?: number;
+ editor_plate_max_lines?: number;
}
const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]);
@@ -118,6 +122,15 @@ function getUtf8ByteSize(value: string): number {
return new TextEncoder().encode(value).byteLength;
}
+function countLines(value: string): number {
+ if (!value) return 0;
+ let count = 1;
+ for (let i = 0; i < value.length; i++) {
+ if (value.charCodeAt(i) === 10) count++;
+ }
+ return count;
+}
+
function formatBytes(bytes: number): string {
if (bytes >= 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
@@ -184,10 +197,17 @@ export function EditorPanelContent({
);
const plateMaxBytes = editorDoc?.editor_plate_max_bytes ?? LARGE_DOCUMENT_THRESHOLD;
- const isLargeDocument = (editorDoc?.content_size_bytes ?? 0) > plateMaxBytes;
+ const plateMaxLines = editorDoc?.editor_plate_max_lines ?? LARGE_DOCUMENT_LINE_THRESHOLD;
+ const docSizeBytes = editorDoc?.content_size_bytes ?? 0;
+ const docLineCount =
+ editorDoc?.line_count ??
+ (editorDoc?.source_markdown ? countLines(editorDoc.source_markdown) : 0);
+ const isLargeDocument = docSizeBytes > plateMaxBytes || docLineCount > plateMaxLines;
const viewerMode: ViewerMode = isMemoryMode
? "plate"
- : (editorDoc?.viewer_mode ?? (isLargeDocument ? "monaco" : "plate"));
+ : editorDoc?.viewer_mode === "monaco" || isLargeDocument
+ ? "monaco"
+ : "plate";
useEffect(() => {
const controller = new AbortController();
@@ -421,7 +441,8 @@ export function EditorPanelContent({
setEditedMarkdown(null);
if (!options?.silent) {
const savedSizeBytes = getUtf8ByteSize(markdownRef.current);
- if (savedSizeBytes > plateMaxBytes) {
+ const savedLineCount = countLines(markdownRef.current);
+ if (savedSizeBytes > plateMaxBytes || savedLineCount > plateMaxLines) {
toast.success("Document saved. It will reopen in raw markdown mode.");
} else {
toast.success("Document saved! Reindexing in background...");
@@ -447,6 +468,7 @@ export function EditorPanelContent({
memoryLimits,
memoryScope,
plateMaxBytes,
+ plateMaxLines,
resolveLocalVirtualPath,
searchSpaceId,
]
@@ -467,8 +489,12 @@ export function EditorPanelContent({
const localFileLanguage = inferMonacoLanguageFromPath(localFilePath);
const activeMarkdown = editedMarkdown ?? editorDoc?.source_markdown ?? "";
const activeMarkdownSizeBytes = useMemo(() => getUtf8ByteSize(activeMarkdown), [activeMarkdown]);
- const isNearPlateLimit = activeMarkdownSizeBytes >= plateMaxBytes * 0.9;
- const isOverPlateLimit = activeMarkdownSizeBytes > plateMaxBytes;
+ const activeMarkdownLineCount = useMemo(() => countLines(activeMarkdown), [activeMarkdown]);
+ const isNearPlateLimit =
+ activeMarkdownSizeBytes >= plateMaxBytes * 0.9 ||
+ activeMarkdownLineCount >= plateMaxLines * 0.9;
+ const isOverPlateLimit =
+ activeMarkdownSizeBytes > plateMaxBytes || activeMarkdownLineCount > plateMaxLines;
const showPlateSizeWarning =
showEditingActions && !isMemoryMode && !isLocalFileMode && isNearPlateLimit;
const memoryLimitState = isMemoryMode
@@ -481,6 +507,13 @@ export function EditorPanelContent({
? "text-orange-500"
: "text-muted-foreground";
const saveDisabled = saving || !hasUnsavedChanges || (memoryLimitState?.isOverLimit ?? false);
+ const editorInstanceKey = `${
+ isMemoryMode
+ ? `memory-${memoryScope ?? "user"}`
+ : isLocalFileMode
+ ? (localFilePath ?? "local-file")
+ : documentId
+ }-${isEditing ? "editing" : "viewing"}`;
const handleCancelEditing = useCallback(() => {
const savedContent = editorDoc?.source_markdown ?? "";
@@ -525,7 +558,7 @@ export function EditorPanelContent({
This document is too large for the editor (
- {Math.round((editorDoc.content_size_bytes ?? 0) / 1024 / 1024)}MB,{" "}
+ {formatBytes(editorDoc.content_size_bytes ?? 0)}, {docLineCount.toLocaleString()} lines,{" "}
{editorDoc.chunk_count ?? 0} chunks). Showing raw markdown below.
{isOverPlateLimit
- ? `This document is ${formatBytes(activeMarkdownSizeBytes)}, above the rich editor limit of ${formatBytes(plateMaxBytes)}. You can save, but it will reopen in raw markdown mode.`
- : `This document is approaching the rich editor limit (${formatBytes(activeMarkdownSizeBytes)} of ${formatBytes(plateMaxBytes)}).`}
+ ? `This document is ${formatBytes(activeMarkdownSizeBytes)} and ${activeMarkdownLineCount.toLocaleString()} lines, above the rich editor limit of ${formatBytes(plateMaxBytes)} or ${plateMaxLines.toLocaleString()} lines. You can save, but it will reopen in raw markdown mode.`
+ : `This document is approaching the rich editor limit (${formatBytes(activeMarkdownSizeBytes)} of ${formatBytes(plateMaxBytes)}, ${activeMarkdownLineCount.toLocaleString()} of ${plateMaxLines.toLocaleString()} lines).`}
)}
) : (
diff --git a/surfsense_web/components/editor/plate-error-boundary.tsx b/surfsense_web/components/editor/plate-error-boundary.tsx
new file mode 100644
index 000000000..c5c18f5e0
--- /dev/null
+++ b/surfsense_web/components/editor/plate-error-boundary.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { Component, type ReactNode } from "react";
+
+interface PlateErrorBoundaryProps {
+ children: ReactNode;
+ fallback: ReactNode;
+}
+
+interface PlateErrorBoundaryState {
+ hasError: boolean;
+}
+
+export class PlateErrorBoundary extends Component<
+ PlateErrorBoundaryProps,
+ PlateErrorBoundaryState
+> {
+ constructor(props: PlateErrorBoundaryProps) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(): PlateErrorBoundaryState {
+ return { hasError: true };
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+
+ return this.props.children;
+ }
+}