"use client"; import { AlertCircle, Pencil } from "lucide-react"; 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 { Button } from "@/components/ui/button"; import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; interface DocumentContent { document_id: number; title: string; document_type?: string; source_markdown: string; } function DocumentSkeleton() { return (
); } interface DocumentTabContentProps { documentId: number; searchSpaceId: number; title?: string; } 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 [editedMarkdown, setEditedMarkdown] = useState(null); const markdownRef = useRef(""); const initialLoadDone = useRef(false); const changeCountRef = useRef(0); useEffect(() => { let cancelled = false; setIsLoading(true); setError(null); setDoc(null); setIsEditing(false); setEditedMarkdown(null); initialLoadDone.current = false; changeCountRef.current = 0; const fetchContent = async () => { const token = getBearerToken(); if (!token) { redirectToLogin(); return; } try { const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`, { method: "GET" } ); if (cancelled) 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 (cancelled) return; console.error("Error fetching document:", err); setError(err instanceof Error ? err.message : "Failed to fetch document"); } finally { if (!cancelled) setIsLoading(false); } }; fetchContent(); return () => { cancelled = true; }; }, [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) { return (

Failed to load document

{error || "An unknown error occurred"}

); } if (isEditing) { return (

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

{editedMarkdown !== null && (

Unsaved changes

)}
); } return (

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

{doc.document_type === "NOTE" && ( )}
); }