diff --git a/surfsense_backend/app/routes/editor_routes.py b/surfsense_backend/app/routes/editor_routes.py index f248b37b1..4579ec874 100644 --- a/surfsense_backend/app/routes/editor_routes.py +++ b/surfsense_backend/app/routes/editor_routes.py @@ -36,7 +36,7 @@ async def get_editor_content( .join(SearchSpace) .filter(Document.id == document_id, SearchSpace.user_id == user.id) ) - document = result.scalars().first() # ✅ Changed from scalar_one_or_none() + document = result.scalars().first() if not document: raise HTTPException(status_code=404, detail="Document not found") @@ -74,7 +74,7 @@ async def update_blocknote_content( .join(SearchSpace) .filter(Document.id == document_id, SearchSpace.user_id == user.id) ) - document = result.scalars().first() # ✅ Changed from scalar_one_or_none() + document = result.scalars().first() if not document: raise HTTPException(status_code=404, detail="Document not found") diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index fb5709608..5371111f3 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -2,7 +2,14 @@ import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { motion } from "motion/react"; +import { Loader2, Save, X, FileText, AlertCircle } from "lucide-react"; +import { toast } from "sonner"; import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; interface EditorContent { document_id: number; @@ -21,6 +28,7 @@ export default function EditorPage() { const [saving, setSaving] = useState(false); const [editorContent, setEditorContent] = useState(null); const [error, setError] = useState(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // Get auth token const token = typeof window !== "undefined" @@ -77,13 +85,20 @@ export default function EditorPage() { } }, [documentId, token]); + // Track changes to mark as unsaved + useEffect(() => { + if (editorContent && document) { + setHasUnsavedChanges(true); + } + }, [editorContent, document]); + // Auto-save every 30 seconds - DIRECT CALL TO FASTAPI useEffect(() => { - if (!editorContent || !token) return; + if (!editorContent || !token || !hasUnsavedChanges) return; const interval = setInterval(async () => { try { - await fetch( + const response = await fetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/blocknote-content`, { method: "PUT", @@ -94,24 +109,28 @@ export default function EditorPage() { body: JSON.stringify({ blocknote_document: editorContent }), } ); - console.log("Auto-saved"); + + if (response.ok) { + setHasUnsavedChanges(false); + toast.success("Auto-saved", { duration: 2000 }); + } } catch (error) { console.error("Auto-save failed:", error); } }, 30000); // 30 seconds return () => clearInterval(interval); - }, [editorContent, documentId, token]); + }, [editorContent, documentId, token, hasUnsavedChanges]); // Save and exit - DIRECT CALL TO FASTAPI const handleSave = async () => { if (!token) { - alert("Please login to save"); + toast.error("Please login to save"); return; } if (!editorContent) { - alert("No content to save"); + toast.error("No content to save"); return; } @@ -135,75 +154,144 @@ export default function EditorPage() { throw new Error(errorData.detail || "Failed to save document"); } - // Redirect back to documents list - router.push(`/dashboard/${params.search_space_id}/documents`); + setHasUnsavedChanges(false); + toast.success("Document saved successfully"); + + // Small delay before redirect to show success message + setTimeout(() => { + router.push(`/dashboard/${params.search_space_id}/documents`); + }, 500); } catch (error) { console.error("Error saving document:", error); - alert(error instanceof Error ? error.message : "Failed to save document. Please try again."); + toast.error(error instanceof Error ? error.message : "Failed to save document. Please try again."); } finally { setSaving(false); } }; + const handleCancel = () => { + if (hasUnsavedChanges) { + if (confirm("You have unsaved changes. Are you sure you want to leave?")) { + router.back(); + } + } else { + router.back(); + } + }; + if (loading) { - return
Loading editor...
; + return ( +
+ + + +

Loading editor...

+
+
+
+ ); } if (error) { return ( - //
-
-
-

Error

-

{error}

- -
+
+ + + +
+ + Error +
+ {error} +
+ + + +
+
); } if (!document) { - return
Document not found
; + return ( +
+ + + +

Document not found

+
+
+
+ ); } return ( - //
-
+ {/* Toolbar */} -
-

{document.title}

-
- - + {saving ? ( + <> + + Saving... + + ) : ( + <> + + Save & Exit + + )} +
- {/* Editor - Now using dynamic import */} -
- + {/* Editor Container */} +
+
+
+ +
+
-
+
); } diff --git a/surfsense_web/components/BlockNoteEditor.tsx b/surfsense_web/components/BlockNoteEditor.tsx index e43d70fc7..57a6d380a 100644 --- a/surfsense_web/components/BlockNoteEditor.tsx +++ b/surfsense_web/components/BlockNoteEditor.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useMemo } from "react"; +import { useTheme } from "next-themes"; import "@blocknote/core/fonts/inter.css"; import "@blocknote/mantine/style.css"; import { useCreateBlockNote } from "@blocknote/react"; @@ -15,6 +16,8 @@ export default function BlockNoteEditor({ initialContent, onChange, }: BlockNoteEditorProps) { + const { resolvedTheme } = useTheme(); + // Track the initial content to prevent re-initialization const initialContentRef = useRef(null); const isInitializedRef = useRef(false); @@ -48,6 +51,21 @@ export default function BlockNoteEditor({ }; }, [editor, onChange]); + // Determine theme for BlockNote with custom dark mode background + const blockNoteTheme = useMemo(() => { + if (resolvedTheme === "dark") { + // Custom dark theme - only override editor background, let BlockNote handle the rest + return { + colors: { + editor: { + background: "#0A0A0A", // Custom dark background + }, + }, + }; + } + return "light" as const; + }, [resolvedTheme]); + // Renders the editor instance - return ; + return ; }