"use client"; import { useAtomValue, useSetAtom } from "jotai"; 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 { 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 (
); } export function EditorPanelContent({ documentId, searchSpaceId, title, onClose, }: { documentId: number; searchSpaceId: number; title: string | null; onClose?: () => void; }) { const [editorDoc, setEditorDoc] = useState(null); 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(""); const initialLoadDone = useRef(false); const changeCountRef = useRef(0); const [displayTitle, setDisplayTitle] = useState(title || "Untitled"); const isLargeDocument = (editorDoc?.content_size_bytes ?? 0) > LARGE_DOCUMENT_THRESHOLD; useEffect(() => { const controller = new AbortController(); setIsLoading(true); setError(null); setEditorDoc(null); setEditedMarkdown(null); initialLoadDone.current = false; changeCountRef.current = 0; const doFetch = async () => { const token = getBearerToken(); if (!token) { redirectToLogin(); return; } try { 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 (controller.signal.aborted) return; if (!response.ok) { const errorData = await response .json() .catch(() => ({ detail: "Failed to fetch document" })); throw new Error(errorData.detail || "Failed to fetch document"); } const data = await response.json(); if (data.source_markdown === undefined || data.source_markdown === null) { setError( "This document does not have editable content. Please re-upload to enable editing." ); setIsLoading(false); return; } markdownRef.current = data.source_markdown; setDisplayTitle(data.title || title || "Untitled"); setEditorDoc(data); initialLoadDone.current = true; } catch (err) { if (controller.signal.aborted) return; console.error("Error fetching document:", err); setError(err instanceof Error ? err.message : "Failed to fetch document"); } finally { if (!controller.signal.aborted) setIsLoading(false); } }; doFetch().catch(() => {}); return () => controller.abort(); }, [documentId, searchSpaceId, title]); const handleMarkdownChange = useCallback((md: string) => { markdownRef.current = md; if (!initialLoadDone.current) return; changeCountRef.current += 1; if (changeCountRef.current <= 1) return; setEditedMarkdown(md); }, []); const handleSave = useCallback(async () => { const token = getBearerToken(); if (!token) { toast.error("Please login to save"); redirectToLogin(); return; } setSaving(true); try { const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ source_markdown: markdownRef.current }), } ); if (!response.ok) { const errorData = await response .json() .catch(() => ({ detail: "Failed to save document" })); throw new Error(errorData.detail || "Failed to save document"); } setEditorDoc((prev) => (prev ? { ...prev, source_markdown: markdownRef.current } : prev)); setEditedMarkdown(null); toast.success("Document saved! Reindexing in background..."); } catch (err) { console.error("Error saving document:", err); toast.error(err instanceof Error ? err.message : "Failed to save document"); } finally { setSaving(false); } }, [documentId, searchSpaceId]); const isEditableType = editorDoc ? EDITABLE_DOCUMENT_TYPES.has(editorDoc.document_type ?? "") && !isLargeDocument : false; return ( <>

{displayTitle}

{isEditableType && editedMarkdown !== null && (

Unsaved changes

)}
{editorDoc?.document_type && ( )} {onClose && ( )}
{isLoading ? ( ) : error || !editorDoc ? (
{error?.toLowerCase().includes("still being processed") ? (
) : (
)}

{error?.toLowerCase().includes("still being processed") ? "Document is processing" : "Document unavailable"}

{error || "An unknown error occurred"}

) : isLargeDocument ? (
This document is too large for the editor ( {Math.round((editorDoc.content_size_bytes ?? 0) / 1024 / 1024)}MB,{" "} {editorDoc.chunk_count ?? 0} chunks). Showing a preview below.
) : isEditableType ? ( ) : (
)}
); } function DesktopEditorPanel() { const panelState = useAtomValue(editorPanelAtom); const closePanel = useSetAtom(closeEditorPanelAtom); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") closePanel(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [closePanel]); if (!panelState.isOpen || !panelState.documentId || !panelState.searchSpaceId) return null; return (
); } function MobileEditorDrawer() { const panelState = useAtomValue(editorPanelAtom); const closePanel = useSetAtom(closeEditorPanelAtom); if (!panelState.documentId || !panelState.searchSpaceId) return null; return ( { if (!open) closePanel(); }} shouldScaleBackground={false} > {panelState.title || "Editor"}
); } export function EditorPanel() { const panelState = useAtomValue(editorPanelAtom); const isDesktop = useMediaQuery("(min-width: 1024px)"); if (!panelState.isOpen || !panelState.documentId) return null; if (isDesktop) { return ; } return ; } export function MobileEditorPanel() { const panelState = useAtomValue(editorPanelAtom); const isDesktop = useMediaQuery("(min-width: 1024px)"); if (isDesktop || !panelState.isOpen || !panelState.documentId) return null; return ; }