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 4888faceb..e0984801e 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
@@ -1,11 +1,13 @@
"use client";
import { useQueryClient } from "@tanstack/react-query";
+import { useAtom } from "jotai";
import { AlertCircle, ArrowLeft, FileText, Loader2, Save } from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
+import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms";
import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor";
import {
AlertDialog,
@@ -76,6 +78,46 @@ export default function EditorPage() {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
+ // Global state for cross-component communication
+ const [, setGlobalHasUnsavedChanges] = useAtom(hasUnsavedEditorChangesAtom);
+ const [pendingNavigation, setPendingNavigation] = useAtom(pendingEditorNavigationAtom);
+
+ // Sync local unsaved changes state with global atom
+ useEffect(() => {
+ setGlobalHasUnsavedChanges(hasUnsavedChanges);
+ }, [hasUnsavedChanges, setGlobalHasUnsavedChanges]);
+
+ // Cleanup global state when component unmounts
+ useEffect(() => {
+ return () => {
+ setGlobalHasUnsavedChanges(false);
+ setPendingNavigation(null);
+ };
+ }, [setGlobalHasUnsavedChanges, setPendingNavigation]);
+
+ // Handle pending navigation from sidebar (e.g., when user clicks "+" to create new note)
+ useEffect(() => {
+ if (pendingNavigation) {
+ if (hasUnsavedChanges) {
+ // Show dialog to confirm navigation
+ setShowUnsavedDialog(true);
+ } else {
+ // No unsaved changes, navigate immediately
+ router.push(pendingNavigation);
+ setPendingNavigation(null);
+ }
+ }
+ }, [pendingNavigation, hasUnsavedChanges, router, setPendingNavigation]);
+
+ // Reset state when documentId changes (e.g., navigating from existing note to new note)
+ useEffect(() => {
+ setDocument(null);
+ setEditorContent(null);
+ setError(null);
+ setHasUnsavedChanges(false);
+ setLoading(true);
+ }, [documentId]);
+
// Fetch document content - DIRECT CALL TO FASTAPI
// Skip fetching if this is a new note
useEffect(() => {
@@ -287,7 +329,23 @@ export default function EditorPage() {
const handleConfirmLeave = () => {
setShowUnsavedDialog(false);
- router.push(`/dashboard/${searchSpaceId}/researcher`);
+ // Clear global unsaved state
+ setGlobalHasUnsavedChanges(false);
+ setHasUnsavedChanges(false);
+
+ // If there's a pending navigation (from sidebar), use that; otherwise go back to researcher
+ if (pendingNavigation) {
+ router.push(pendingNavigation);
+ setPendingNavigation(null);
+ } else {
+ router.push(`/dashboard/${searchSpaceId}/researcher`);
+ }
+ };
+
+ const handleCancelLeave = () => {
+ setShowUnsavedDialog(false);
+ // Clear pending navigation if user cancels
+ setPendingNavigation(null);
};
if (loading) {
@@ -402,6 +460,7 @@ export default function EditorPage() {
)}
{/* Unsaved Changes Dialog */}
-
+ {
+ if (!open) handleCancelLeave();
+ }}>
Unsaved Changes
@@ -420,7 +481,7 @@ export default function EditorPage() {
- Cancel
+ Cancel
OK
diff --git a/surfsense_web/atoms/editor/ui.atoms.ts b/surfsense_web/atoms/editor/ui.atoms.ts
new file mode 100644
index 000000000..81a89a945
--- /dev/null
+++ b/surfsense_web/atoms/editor/ui.atoms.ts
@@ -0,0 +1,27 @@
+import { atom } from "jotai";
+
+interface EditorUIState {
+ hasUnsavedChanges: boolean;
+ pendingNavigation: string | null; // URL to navigate to after user confirms
+}
+
+export const editorUIAtom = atom({
+ hasUnsavedChanges: false,
+ pendingNavigation: null,
+});
+
+// Derived atom for just the unsaved changes state
+export const hasUnsavedEditorChangesAtom = atom(
+ (get) => get(editorUIAtom).hasUnsavedChanges,
+ (get, set, value: boolean) => {
+ set(editorUIAtom, { ...get(editorUIAtom), hasUnsavedChanges: value });
+ }
+);
+
+// Derived atom for pending navigation
+export const pendingEditorNavigationAtom = atom(
+ (get) => get(editorUIAtom).pendingNavigation,
+ (get, set, value: string | null) => {
+ set(editorUIAtom, { ...get(editorUIAtom), pendingNavigation: value });
+ }
+);
diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx
index 393d86b74..0e4ca0460 100644
--- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx
+++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx
@@ -9,6 +9,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms";
import { chatsAtom } from "@/atoms/chats/chat-query.atoms";
import { globalChatsQueryParamsAtom } from "@/atoms/chats/ui.atoms";
+import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { Button } from "@/components/ui/button";
@@ -55,6 +56,10 @@ export function AppSidebarProvider({
const { data: chats, error: chatError, isLoading: isLoadingChats } = useAtomValue(chatsAtom);
const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] =
useAtom(deleteChatMutationAtom);
+
+ // Editor state for handling unsaved changes
+ const hasUnsavedEditorChanges = useAtomValue(hasUnsavedEditorChangesAtom);
+ const setPendingNavigation = useSetAtom(pendingEditorNavigationAtom);
useEffect(() => {
setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 4 }));
@@ -233,10 +238,18 @@ export function AppSidebarProvider({
}));
}, [notesData]);
- // Handle add note
+ // Handle add note - check for unsaved changes first
const handleAddNote = useCallback(() => {
- router.push(`/dashboard/${searchSpaceId}/editor/new`);
- }, [router, searchSpaceId]);
+ const newNoteUrl = `/dashboard/${searchSpaceId}/editor/new`;
+
+ if (hasUnsavedEditorChanges) {
+ // Set pending navigation - the editor will show the unsaved changes dialog
+ setPendingNavigation(newNoteUrl);
+ } else {
+ // No unsaved changes, navigate directly
+ router.push(newNoteUrl);
+ }
+ }, [router, searchSpaceId, hasUnsavedEditorChanges, setPendingNavigation]);
// Memoized updated navSecondary
const updatedNavSecondary = useMemo(() => {
diff --git a/surfsense_web/contracts/types/chat.types.ts b/surfsense_web/contracts/types/chat.types.ts
index 0b8b5975c..a4e5b8f5b 100644
--- a/surfsense_web/contracts/types/chat.types.ts
+++ b/surfsense_web/contracts/types/chat.types.ts
@@ -14,7 +14,7 @@ export const chatSummary = z.object({
});
export const chatDetails = chatSummary.extend({
- initial_connectors: z.array(z.string()),
+ initial_connectors: z.array(z.string()).nullable().optional(),
messages: z.array(z.any()),
});