diff --git a/surfsense_backend/app/routes/editor_routes.py b/surfsense_backend/app/routes/editor_routes.py index 34828964a..5ab8c931c 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 7e1e429e2..75283c81f 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(); @@ -423,7 +443,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..."); @@ -449,6 +470,7 @@ export function EditorPanelContent({ memoryLimits, memoryScope, plateMaxBytes, + plateMaxLines, resolveLocalVirtualPath, searchSpaceId, ] @@ -469,8 +491,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 @@ -483,6 +509,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 ?? ""; @@ -529,7 +562,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.