"use client"; import { Download, FileQuestionMark, FileText, Loader2, PenLine, RefreshCw } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { PlateEditor } from "@/components/editor/plate-editor"; import { MarkdownViewer } from "@/components/markdown-viewer"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB interface DocumentContent { document_id: number; title: string; document_type?: string; source_markdown: string; content_size_bytes?: number; chunk_count?: number; truncated?: boolean; } function DocumentSkeleton() { return (
); } interface DocumentTabContentProps { documentId: number; searchSpaceId: number; title?: string; } const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]); export function DocumentTabContent({ documentId, searchSpaceId, title }: DocumentTabContentProps) { const [doc, setDoc] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isEditing, setIsEditing] = useState(false); 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 router = useRouter(); const isLargeDocument = (doc?.content_size_bytes ?? 0) > LARGE_DOCUMENT_THRESHOLD; useEffect(() => { const controller = new AbortController(); setIsLoading(true); setError(null); setDoc(null); setIsEditing(false); 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 viewable content."); setIsLoading(false); return; } markdownRef.current = data.source_markdown; setDoc(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]); 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"); } setDoc((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]); if (isLoading) return ; if (error || !doc) { const isProcessing = error?.toLowerCase().includes("still being processed"); return (
{isProcessing ? ( ) : ( )}

{isProcessing ? "Document is processing" : "Document unavailable"}

{error || "An unknown error occurred"}

{!isProcessing && ( )}
); } const isEditable = EDITABLE_DOCUMENT_TYPES.has(doc.document_type ?? "") && !isLargeDocument; if (isEditing && !isLargeDocument) { return (

{doc.title || title || "Untitled"}

{editedMarkdown !== null && (

Unsaved changes

)}
); } return (

{doc.title || title || "Untitled"}

{isEditable && ( )}
{isLargeDocument ? ( <> 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. ) : ( )}
); }