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 9e188b09a..578c838b7 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,6 +1,6 @@ "use client"; -import { AlertCircle, ArrowLeft, FileText, Loader2, Save, X } from "lucide-react"; +import { AlertCircle, ArrowLeft, FileText, Loader2, Plus, SquarePen, Save, X } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; @@ -19,6 +19,7 @@ import { import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; +import { notesApiService } from "@/lib/apis/notes-api.service"; import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; interface EditorContent { @@ -62,6 +63,8 @@ export default function EditorPage() { const params = useParams(); const router = useRouter(); const documentId = params.documentId as string; + const searchSpaceId = Number(params.search_space_id); + const isNewNote = documentId === "new"; const [document, setDocument] = useState(null); const [loading, setLoading] = useState(true); @@ -72,8 +75,23 @@ export default function EditorPage() { const [showUnsavedDialog, setShowUnsavedDialog] = useState(false); // Fetch document content - DIRECT CALL TO FASTAPI + // Skip fetching if this is a new note useEffect(() => { async function fetchDocument() { + // For new notes, initialize with empty state + if (isNewNote) { + setDocument({ + document_id: 0, + title: "Untitled", + document_type: "NOTE", + blocknote_document: null, + updated_at: null, + }); + setEditorContent(null); + setLoading(false); + return; + } + const token = getBearerToken(); if (!token) { console.error("No auth token found"); @@ -122,7 +140,7 @@ export default function EditorPage() { if (documentId) { fetchDocument(); } - }, [documentId, params.search_space_id]); + }, [documentId, params.search_space_id, isNewNote]); // Track changes to mark as unsaved useEffect(() => { @@ -132,7 +150,7 @@ export default function EditorPage() { }, [editorContent, document]); // Check if this is a NOTE type document - const isNote = document?.document_type === "NOTE"; + const isNote = isNewNote || document?.document_type === "NOTE"; // Extract title dynamically from editor content for notes, otherwise use document title const displayTitle = useMemo(() => { @@ -145,6 +163,7 @@ export default function EditorPage() { // TODO: Maybe add Auto-save every 30 seconds - DIRECT CALL TO FASTAPI // Save and exit - DIRECT CALL TO FASTAPI + // For new notes, create the note first, then save const handleSave = async () => { const token = getBearerToken(); if (!token) { @@ -153,42 +172,90 @@ export default function EditorPage() { return; } - if (!editorContent) { - toast.error("No content to save"); - return; - } - setSaving(true); + setError(null); + try { - // Save blocknote_document and trigger reindexing in background - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${params.search_space_id}/documents/${documentId}/save`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ blocknote_document: editorContent }), + // If this is a new note, create it first + if (isNewNote) { + const title = extractTitleFromBlockNote(editorContent); + + // Create the note first + const note = await notesApiService.createNote({ + search_space_id: searchSpaceId, + title: title, + blocknote_document: editorContent || undefined, + }); + + // If there's content, save it properly and trigger reindexing + if (editorContent) { + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${note.id}/save`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ blocknote_document: editorContent }), + } + ); + + if (!response.ok) { + const errorData = await response + .json() + .catch(() => ({ detail: "Failed to save document" })); + throw new Error(errorData.detail || "Failed to save document"); + } } - ); - if (!response.ok) { - const errorData = await response - .json() - .catch(() => ({ detail: "Failed to save document" })); - throw new Error(errorData.detail || "Failed to save document"); + setHasUnsavedChanges(false); + toast.success("Note created successfully! Reindexing in background..."); + + // Redirect to editor with the new document ID + setTimeout(() => { + router.push(`/dashboard/${searchSpaceId}/editor/${note.id}`); + }, 500); + } else { + // Existing document - save normally + if (!editorContent) { + toast.error("No content to save"); + setSaving(false); + return; + } + + // Save blocknote_document and trigger reindexing in background + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${params.search_space_id}/documents/${documentId}/save`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ blocknote_document: editorContent }), + } + ); + + if (!response.ok) { + const errorData = await response + .json() + .catch(() => ({ detail: "Failed to save document" })); + throw new Error(errorData.detail || "Failed to save document"); + } + + setHasUnsavedChanges(false); + toast.success("Document saved! Reindexing in background..."); + + // Small delay before redirect to show success message + setTimeout(() => { + router.push(`/dashboard/${params.search_space_id}/documents`); + }, 500); } - - setHasUnsavedChanges(false); - toast.success("Document saved! Reindexing in background..."); - - // 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); - toast.error( - error instanceof Error ? error.message : "Failed to save document. Please try again." - ); + const errorMessage = + error instanceof Error + ? error.message + : isNewNote + ? "Failed to create note. Please try again." + : "Failed to save document. Please try again."; + setError(errorMessage); + toast.error(errorMessage); } finally { setSaving(false); } @@ -248,7 +315,7 @@ export default function EditorPage() { ); } - if (!document) { + if (!document && !isNewNote) { return (
@@ -286,13 +353,20 @@ export default function EditorPage() { {saving ? ( <> - Saving... + {isNewNote ? "Creating..." : "Saving..."} ) : ( - <> - - Save & Exit - + isNewNote ? ( + <> + + Create Note + + ) : ( + <> + + Save & Exit + + ) )}
@@ -301,9 +375,21 @@ export default function EditorPage() { {/* Editor Container */}
+ {error && ( + +
+ +

{error}

+
+
+ )}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/new/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/new/page.tsx deleted file mode 100644 index 8e3d2c911..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/new/page.tsx +++ /dev/null @@ -1,175 +0,0 @@ -"use client"; - -import { AlertCircle, ArrowLeft, FileText, Loader2, Plus } from "lucide-react"; -import { motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; -import { useMemo, useState } from "react"; -import { toast } from "sonner"; -import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor"; -import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; -import { notesApiService } from "@/lib/apis/notes-api.service"; -import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; - -// Helper function to extract title from BlockNote document -// Takes the text content from the first block (should be a heading) -function extractTitleFromBlockNote(blocknoteDocument: any[] | null | undefined): string { - if (!blocknoteDocument || !Array.isArray(blocknoteDocument) || blocknoteDocument.length === 0) { - return "Untitled"; - } - - const firstBlock = blocknoteDocument[0]; - if (!firstBlock) { - return "Untitled"; - } - - // Extract text from block content - // BlockNote blocks have a content array with inline content - if (firstBlock.content && Array.isArray(firstBlock.content)) { - const textContent = firstBlock.content - .map((item: any) => { - if (typeof item === "string") return item; - if (item?.text) return item.text; - return ""; - }) - .join("") - .trim(); - return textContent || "Untitled"; - } - - return "Untitled"; -} - -export default function NewNotePage() { - const params = useParams(); - const router = useRouter(); - const searchSpaceId = Number(params.search_space_id); - - const [editorContent, setEditorContent] = useState(null); - const [creating, setCreating] = useState(false); - const [error, setError] = useState(null); - - // Extract title dynamically from editor content - const dynamicTitle = useMemo(() => { - return extractTitleFromBlockNote(editorContent); - }, [editorContent]); - - const handleCreate = async () => { - setCreating(true); - setError(null); - - try { - // Extract title from first block of editor content - const title = extractTitleFromBlockNote(editorContent); - - // Create the note first - const note = await notesApiService.createNote({ - search_space_id: searchSpaceId, - title: title, - blocknote_document: editorContent || undefined, - }); - - // If there's content, save it properly and trigger reindexing - if (editorContent) { - const token = getBearerToken(); - if (!token) { - toast.error("Please login to save"); - redirectToLogin(); - return; - } - - // Call the save endpoint to properly save blocknote_document and trigger reindexing - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${note.id}/save`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ blocknote_document: editorContent }), - } - ); - - if (!response.ok) { - const errorData = await response - .json() - .catch(() => ({ detail: "Failed to save document" })); - throw new Error(errorData.detail || "Failed to save document"); - } - } - - toast.success("Note created successfully! Reindexing in background..."); - // Redirect to editor - router.push(`/dashboard/${searchSpaceId}/editor/${note.id}`); - } catch (error) { - console.error("Error creating note:", error); - const errorMessage = - error instanceof Error ? error.message : "Failed to create note. Please try again."; - setError(errorMessage); - toast.error(errorMessage); - } finally { - setCreating(false); - } - }; - - const handleBack = () => { - router.back(); - }; - - return ( - - {/* Toolbar */} -
-
- -
-

{dynamicTitle}

-
-
- -
- - -
-
- - {/* Editor Container - matches editor page layout */} -
-
- {error && ( - -
- -

{error}

-
-
- )} -
- -
-
-
-
- ); -} \ No newline at end of file diff --git a/surfsense_web/components/dashboard-breadcrumb.tsx b/surfsense_web/components/dashboard-breadcrumb.tsx index 6335f9f1a..c6b264879 100644 --- a/surfsense_web/components/dashboard-breadcrumb.tsx +++ b/surfsense_web/components/dashboard-breadcrumb.tsx @@ -44,6 +44,13 @@ export function DashboardBreadcrumb() { useEffect(() => { if (segments[2] === "editor" && segments[3] && searchSpaceId) { const documentId = segments[3]; + + // Skip fetch for "new" notes + if (documentId === "new") { + setDocumentTitle(null); + return; + } + const token = getBearerToken(); if (token) { @@ -110,7 +117,14 @@ export function DashboardBreadcrumb() { // Handle editor sub-sections (document ID) if (section === "editor") { - const documentLabel = documentTitle || subSection; + // Handle special cases for editor + let documentLabel: string; + if (subSection === "new") { + documentLabel = "New Note"; + } else { + documentLabel = documentTitle || subSection; + } + breadcrumbs.push({ label: t("documents"), href: `/dashboard/${segments[1]}/documents`, diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 2144dd861..bdae5b5e1 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -138,13 +138,13 @@ export const iconMap: Record = { MessageCircleMore, Settings2, SquareLibrary, + FileText, SquareTerminal, AlertCircle, Info, ExternalLink, Trash2, Podcast, - FileText, Users, }; diff --git a/surfsense_web/components/sidebar/nav-notes.tsx b/surfsense_web/components/sidebar/nav-notes.tsx index 14dd75c59..f392e25b6 100644 --- a/surfsense_web/components/sidebar/nav-notes.tsx +++ b/surfsense_web/components/sidebar/nav-notes.tsx @@ -10,6 +10,7 @@ import { RefreshCw, Share, Trash2, + Eye, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -149,9 +150,9 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true }: NavNotesProps ); return ( - + -
+
- {onAddNote && ( +
- )} + {onAddNote && ( + + )} +
diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index 583ace11e..18521edb7 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -17,7 +17,7 @@ import { IconTicket, IconWorldWww, } from "@tabler/icons-react"; -import { File, Globe, Link, Microscope, Search, Sparkles, Telescope, Webhook } from "lucide-react"; +import { File, FileText, Globe, Link, Microscope, Search, Sparkles, Telescope, Webhook } from "lucide-react"; import { EnumConnectorName } from "./connector"; export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => { @@ -71,6 +71,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas return ; case "FILE": return ; + case "NOTE": + return ; case "EXTENSION": return ; case "DEEP":