diff --git a/surfsense_web/components/chat-comments/comment-composer/comment-composer.tsx b/surfsense_web/components/chat-comments/comment-composer/comment-composer.tsx
index 3e9b4504f..3d6ea384b 100644
--- a/surfsense_web/components/chat-comments/comment-composer/comment-composer.tsx
+++ b/surfsense_web/components/chat-comments/comment-composer/comment-composer.tsx
@@ -15,13 +15,14 @@ function convertDisplayToData(displayContent: string, mentions: InsertedMention[
const sortedMentions = [...mentions].sort((a, b) => b.displayName.length - a.displayName.length);
- for (const mention of sortedMentions) {
- const displayPattern = new RegExp(
- `@${escapeRegExp(mention.displayName)}(?=\\s|$|[.,!?;:])`,
- "g"
- );
- const dataFormat = `@[${mention.id}]`;
- result = result.replace(displayPattern, dataFormat);
+ const mentionPatterns = sortedMentions.map((mention) => ({
+ pattern: new RegExp(`@${escapeRegExp(mention.displayName)}(?=\\s|$|[.,!?;:])`, "g"),
+ dataFormat: `@[${mention.id}]`,
+ }));
+
+ for (const { pattern, dataFormat } of mentionPatterns) {
+ pattern.lastIndex = 0; // reset global regex state
+ result = result.replace(pattern, dataFormat);
}
return result;
diff --git a/surfsense_web/components/editor-panel/editor-panel.tsx b/surfsense_web/components/editor-panel/editor-panel.tsx
index a1195ef33..59af0ee8d 100644
--- a/surfsense_web/components/editor-panel/editor-panel.tsx
+++ b/surfsense_web/components/editor-panel/editor-panel.tsx
@@ -1,18 +1,39 @@
"use client";
import { useAtomValue, useSetAtom } from "jotai";
-import { FileQuestionMark, RefreshCw, XIcon } from "lucide-react";
+import { Download, FileQuestionMark, FileText, Loader2, RefreshCw, XIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { VersionHistoryButton } from "@/components/documents/version-history";
import { MarkdownViewer } from "@/components/markdown-viewer";
+import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
+import { Skeleton } from "@/components/ui/skeleton";
import { useMediaQuery } from "@/hooks/use-media-query";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
+const PlateEditor = dynamic(
+ () => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
+ { ssr: false, loading: () =>
}
+);
+
+const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
+
+interface EditorContent {
+ document_id: number;
+ title: string;
+ document_type?: string;
+ source_markdown: string;
+ content_size_bytes?: number;
+ chunk_count?: number;
+ truncated?: boolean;
+}
+
+const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]);
+
function EditorPanelSkeleton() {
return (
@@ -33,19 +54,6 @@ function EditorPanelSkeleton() {
);
}
-const PlateEditor = dynamic(
- () => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
- { ssr: false, loading: () => null }
-);
-
-interface EditorContent {
- document_id: number;
- title: string;
- document_type?: string;
- source_markdown: string;
-}
-
-const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]);
export function EditorPanelContent({
documentId,
@@ -62,6 +70,7 @@ export function EditorPanelContent({
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState
(null);
const [saving, setSaving] = useState(false);
+ const [downloading, setDownloading] = useState(false);
const [editedMarkdown, setEditedMarkdown] = useState(null);
const markdownRef = useRef("");
@@ -69,6 +78,8 @@ export function EditorPanelContent({
const changeCountRef = useRef(0);
const [displayTitle, setDisplayTitle] = useState(title || "Untitled");
+ const isLargeDocument = (editorDoc?.content_size_bytes ?? 0) > LARGE_DOCUMENT_THRESHOLD;
+
useEffect(() => {
let cancelled = false;
setIsLoading(true);
@@ -86,10 +97,12 @@ export function EditorPanelContent({
}
try {
- const response = await authenticatedFetch(
- `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`,
- { method: "GET" }
+ const url = new URL(
+ `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
);
+ url.searchParams.set("max_length", String(LARGE_DOCUMENT_THRESHOLD));
+
+ const response = await authenticatedFetch(url.toString(), { method: "GET" });
if (cancelled) return;
@@ -175,7 +188,7 @@ export function EditorPanelContent({
}, [documentId, searchSpaceId]);
const isEditableType = editorDoc
- ? EDITABLE_DOCUMENT_TYPES.has(editorDoc.document_type ?? "")
+ ? EDITABLE_DOCUMENT_TYPES.has(editorDoc.document_type ?? "") && !isLargeDocument
: false;
return (
@@ -223,6 +236,59 @@ export function EditorPanelContent({
{error || "An unknown error occurred"}
+ ) : isLargeDocument ? (
+