feat(editor): implement viewer mode for large documents, switching between 'plate' and 'monaco' editors based on document size

This commit is contained in:
Anish Sarkar 2026-06-05 14:23:18 +05:30
parent c2b8b3ac5e
commit cf8a2d0759
4 changed files with 100 additions and 71 deletions

View file

@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { PlateEditor } from "@/components/editor/plate-editor";
import { SourceCodeEditor } from "@/components/editor/source-code-editor";
import { MarkdownViewer } from "@/components/markdown-viewer";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
@ -22,6 +23,7 @@ interface DocumentContent {
content_size_bytes?: number;
chunk_count?: number;
truncated?: boolean;
viewer_mode?: ViewerMode;
}
function DocumentSkeleton() {
@ -51,6 +53,7 @@ interface DocumentTabContentProps {
}
const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]);
type ViewerMode = "plate" | "monaco";
export function DocumentTabContent({ documentId, searchSpaceId, title }: DocumentTabContentProps) {
const [doc, setDoc] = useState<DocumentContent | null>(null);
@ -66,6 +69,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
const router = useRouter();
const isLargeDocument = (doc?.content_size_bytes ?? 0) > LARGE_DOCUMENT_THRESHOLD;
const viewerMode: ViewerMode = doc?.viewer_mode ?? (isLargeDocument ? "monaco" : "plate");
useEffect(() => {
const controller = new AbortController();
@ -204,9 +208,9 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
);
}
const isEditable = EDITABLE_DOCUMENT_TYPES.has(doc.document_type ?? "") && !isLargeDocument;
const isEditable = viewerMode === "plate" && EDITABLE_DOCUMENT_TYPES.has(doc.document_type ?? "");
if (isEditing && !isLargeDocument) {
if (isEditing && viewerMode === "plate") {
return (
<div className="flex flex-col h-full overflow-hidden">
<div className="flex items-center justify-between px-6 py-3 border-b shrink-0">
@ -265,64 +269,74 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
</Button>
)}
</div>
<div className="flex-1 overflow-auto">
<div className="max-w-4xl mx-auto px-6 py-6">
{isLargeDocument ? (
<>
<Alert className="mb-4">
<FileText className="size-4" />
<AlertDescription className="flex items-center justify-between gap-4">
<span>
This document is too large for the editor (
{Math.round((doc.content_size_bytes ?? 0) / 1024 / 1024)}MB,{" "}
{doc.chunk_count ?? 0} chunks). Showing a preview below.
<div className="flex-1 min-h-0 overflow-hidden">
{viewerMode === "monaco" ? (
<div className="flex h-full min-h-0 flex-col">
<Alert className="m-4 shrink-0">
<FileText className="size-4" />
<AlertDescription className="flex items-center justify-between gap-4">
<span>
This document is too large for the editor (
{Math.round((doc.content_size_bytes ?? 0) / 1024 / 1024)}MB,{" "}
{doc.chunk_count ?? 0} chunks). Showing raw markdown below.
</span>
<Button
variant="outline"
size="sm"
className="relative shrink-0"
disabled={downloading}
onClick={async () => {
setDownloading(true);
try {
const response = await authenticatedFetch(
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
{ method: "GET" }
);
if (!response.ok) throw new Error("Download failed");
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const disposition = response.headers.get("content-disposition");
const match = disposition?.match(/filename="(.+)"/);
a.download = match?.[1] ?? `${doc.title || "document"}.md`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
toast.success("Download started");
} catch {
toast.error("Failed to download document");
} finally {
setDownloading(false);
}
}}
>
<span className={`flex items-center gap-1.5 ${downloading ? "opacity-0" : ""}`}>
<Download className="size-3.5" />
Download .md
</span>
<Button
variant="outline"
size="sm"
className="relative shrink-0"
disabled={downloading}
onClick={async () => {
setDownloading(true);
try {
const response = await authenticatedFetch(
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
{ method: "GET" }
);
if (!response.ok) throw new Error("Download failed");
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const disposition = response.headers.get("content-disposition");
const match = disposition?.match(/filename="(.+)"/);
a.download = match?.[1] ?? `${doc.title || "document"}.md`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
toast.success("Download started");
} catch {
toast.error("Failed to download document");
} finally {
setDownloading(false);
}
}}
>
<span className={`flex items-center gap-1.5 ${downloading ? "opacity-0" : ""}`}>
<Download className="size-3.5" />
Download .md
</span>
{downloading && <Spinner size="sm" className="absolute" />}
</Button>
</AlertDescription>
</Alert>
{downloading && <Spinner size="sm" className="absolute" />}
</Button>
</AlertDescription>
</Alert>
<div className="min-h-0 flex-1 overflow-hidden">
<SourceCodeEditor
path={`${doc.title || "document"}.md`}
language="markdown"
value={doc.source_markdown}
readOnly
onChange={() => {}}
/>
</div>
</div>
) : (
<div className="h-full overflow-auto">
<div className="max-w-4xl mx-auto px-6 py-6">
<MarkdownViewer content={doc.source_markdown} enableCitations />
</>
) : (
<MarkdownViewer content={doc.source_markdown} enableCitations />
)}
</div>
</div>
</div>
)}
</div>
</div>
);