code quality issues fixed

This commit is contained in:
Anish Sarkar 2025-11-23 16:39:23 +05:30
parent abbaa848f3
commit 3fac196c35
17 changed files with 495 additions and 493 deletions

View file

@ -4,6 +4,7 @@ Revision ID: 38
Revises: 37 Revises: 37
""" """
from collections.abc import Sequence from collections.abc import Sequence
import sqlalchemy as sa import sqlalchemy as sa
@ -12,8 +13,8 @@ from sqlalchemy.dialects import postgresql
from alembic import op from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '38' revision: str = "38"
down_revision: str | None = '37' down_revision: str | None = "37"
branch_labels: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None
@ -22,20 +23,28 @@ def upgrade() -> None:
"""Upgrade schema - Add BlockNote fields only.""" """Upgrade schema - Add BlockNote fields only."""
op.add_column( op.add_column(
'documents', "documents",
sa.Column('blocknote_document', postgresql.JSONB(astext_type=sa.Text()), nullable=True), sa.Column(
"blocknote_document", postgresql.JSONB(astext_type=sa.Text()), nullable=True
),
) )
op.add_column( op.add_column(
'documents', "documents",
sa.Column('content_needs_reindexing', sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column(
"content_needs_reindexing",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
),
) )
op.add_column( op.add_column(
'documents', "documents",
sa.Column('last_edited_at', sa.TIMESTAMP(timezone=True), nullable=True) sa.Column("last_edited_at", sa.TIMESTAMP(timezone=True), nullable=True),
) )
def downgrade() -> None: def downgrade() -> None:
"""Downgrade schema - Remove BlockNote fields.""" """Downgrade schema - Remove BlockNote fields."""
op.drop_column('documents', 'last_edited_at') op.drop_column("documents", "last_edited_at")
op.drop_column('documents', 'content_needs_reindexing') op.drop_column("documents", "content_needs_reindexing")
op.drop_column('documents', 'blocknote_document') op.drop_column("documents", "blocknote_document")

View file

@ -1,6 +1,7 @@
""" """
Editor routes for BlockNote document editing. Editor routes for BlockNote document editing.
""" """
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
@ -47,14 +48,16 @@ async def get_editor_content(
"document_id": document.id, "document_id": document.id,
"title": document.title, "title": document.title,
"blocknote_document": document.blocknote_document, "blocknote_document": document.blocknote_document,
"last_edited_at": document.last_edited_at.isoformat() if document.last_edited_at else None, "last_edited_at": document.last_edited_at.isoformat()
if document.last_edited_at
else None,
} }
# For old documents without blocknote_document, return error # For old documents without blocknote_document, return error
# (Can't convert summary back to full document) # (Can't convert summary back to full document)
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="This document was uploaded before editing was enabled. Please re-upload to enable editing." detail="This document was uploaded before editing was enabled. Please re-upload to enable editing.",
) )
@ -158,4 +161,3 @@ async def update_blocknote_content(
# "message": "Document saved. Summary and chunks will be regenerated in the background.", # "message": "Document saved. Summary and chunks will be regenerated in the background.",
# "content_needs_reindexing": True, # "content_needs_reindexing": True,
# } # }

View file

@ -105,8 +105,9 @@ async def add_received_file_document_using_unstructured(
# Convert markdown to BlockNote JSON # Convert markdown to BlockNote JSON
blocknote_json = await convert_markdown_to_blocknote(file_in_markdown) blocknote_json = await convert_markdown_to_blocknote(file_in_markdown)
if not blocknote_json: if not blocknote_json:
logging.warning(f"Failed to convert {file_name} to BlockNote JSON, document will not be editable") logging.warning(
f"Failed to convert {file_name} to BlockNote JSON, document will not be editable"
)
# Update or create document # Update or create document
if existing_document: if existing_document:
@ -229,8 +230,9 @@ async def add_received_file_document_using_llamacloud(
# Convert markdown to BlockNote JSON # Convert markdown to BlockNote JSON
blocknote_json = await convert_markdown_to_blocknote(file_in_markdown) blocknote_json = await convert_markdown_to_blocknote(file_in_markdown)
if not blocknote_json: if not blocknote_json:
logging.warning(f"Failed to convert {file_name} to BlockNote JSON, document will not be editable") logging.warning(
f"Failed to convert {file_name} to BlockNote JSON, document will not be editable"
)
# Update or create document # Update or create document
if existing_document: if existing_document:
@ -378,8 +380,9 @@ async def add_received_file_document_using_docling(
# Convert markdown to BlockNote JSON # Convert markdown to BlockNote JSON
blocknote_json = await convert_markdown_to_blocknote(file_in_markdown) blocknote_json = await convert_markdown_to_blocknote(file_in_markdown)
if not blocknote_json: if not blocknote_json:
logging.warning(f"Failed to convert {file_name} to BlockNote JSON, document will not be editable") logging.warning(
f"Failed to convert {file_name} to BlockNote JSON, document will not be editable"
)
# Update or create document # Update or create document
if existing_document: if existing_document:

View file

@ -115,8 +115,9 @@ async def add_received_markdown_file_document(
# Convert to BlockNote JSON # Convert to BlockNote JSON
blocknote_json = await convert_markdown_to_blocknote(file_in_markdown) blocknote_json = await convert_markdown_to_blocknote(file_in_markdown)
if not blocknote_json: if not blocknote_json:
logging.warning(f"Failed to convert {file_name} to BlockNote JSON, document will not be editable") logging.warning(
f"Failed to convert {file_name} to BlockNote JSON, document will not be editable"
)
# Update or create document # Update or create document
if existing_document: if existing_document:

View file

@ -32,10 +32,10 @@ async def convert_markdown_to_blocknote(markdown: str) -> dict[str, Any] | None:
{ {
"type": "text", "type": "text",
"text": "Document content could not be converted for editing.", "text": "Document content could not be converted for editing.",
"styles": {} "styles": {},
} }
], ],
"children": [] "children": [],
} }
] ]
@ -51,7 +51,9 @@ async def convert_markdown_to_blocknote(markdown: str) -> dict[str, Any] | None:
blocknote_document = data.get("blocknote_document") blocknote_document = data.get("blocknote_document")
if blocknote_document: if blocknote_document:
logger.info(f"Successfully converted markdown to BlockNote (original: {len(markdown)} chars, sanitized: {len(markdown)} chars)") logger.info(
f"Successfully converted markdown to BlockNote (original: {len(markdown)} chars, sanitized: {len(markdown)} chars)"
)
return blocknote_document return blocknote_document
else: else:
logger.warning("Next.js API returned empty blocknote_document") logger.warning("Next.js API returned empty blocknote_document")
@ -61,7 +63,9 @@ async def convert_markdown_to_blocknote(markdown: str) -> dict[str, Any] | None:
logger.error("Timeout converting markdown to BlockNote after 30s") logger.error("Timeout converting markdown to BlockNote after 30s")
return None return None
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"HTTP error converting markdown to BlockNote: {e.response.status_code} - {e.response.text}") logger.error(
f"HTTP error converting markdown to BlockNote: {e.response.status_code} - {e.response.text}"
)
# Log first 1000 chars of problematic markdown for debugging # Log first 1000 chars of problematic markdown for debugging
logger.debug(f"Problematic markdown sample: {markdown[:1000]}") logger.debug(f"Problematic markdown sample: {markdown[:1000]}")
return None return None
@ -69,7 +73,10 @@ async def convert_markdown_to_blocknote(markdown: str) -> dict[str, Any] | None:
logger.error(f"Failed to convert markdown to BlockNote: {e}", exc_info=True) logger.error(f"Failed to convert markdown to BlockNote: {e}", exc_info=True)
return None return None
async def convert_blocknote_to_markdown(blocknote_document: dict[str, Any] | list[dict[str, Any]]) -> str | None:
async def convert_blocknote_to_markdown(
blocknote_document: dict[str, Any] | list[dict[str, Any]],
) -> str | None:
""" """
Convert BlockNote JSON to markdown via Next.js API. Convert BlockNote JSON to markdown via Next.js API.
@ -95,7 +102,9 @@ async def convert_blocknote_to_markdown(blocknote_document: dict[str, Any] | lis
markdown = data.get("markdown") markdown = data.get("markdown")
if markdown: if markdown:
logger.info(f"Successfully converted BlockNote to markdown ({len(markdown)} chars)") logger.info(
f"Successfully converted BlockNote to markdown ({len(markdown)} chars)"
)
return markdown return markdown
else: else:
logger.warning("Next.js API returned empty markdown") logger.warning("Next.js API returned empty markdown")
@ -105,9 +114,10 @@ async def convert_blocknote_to_markdown(blocknote_document: dict[str, Any] | lis
logger.error("Timeout converting BlockNote to markdown after 30s") logger.error("Timeout converting BlockNote to markdown after 30s")
return None return None
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
logger.error(f"HTTP error converting BlockNote to markdown: {e.response.status_code} - {e.response.text}") logger.error(
f"HTTP error converting BlockNote to markdown: {e.response.status_code} - {e.response.text}"
)
return None return None
except Exception as e: except Exception as e:
logger.error(f"Failed to convert BlockNote to markdown: {e}", exc_info=True) logger.error(f"Failed to convert BlockNote to markdown: {e}", exc_info=True)
return None return None

View file

@ -2,42 +2,39 @@ import { ServerBlockNoteEditor } from "@blocknote/server-util";
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { markdown } = await request.json(); const { markdown } = await request.json();
if (!markdown || typeof markdown !== "string") { if (!markdown || typeof markdown !== "string") {
return NextResponse.json( return NextResponse.json({ error: "Markdown string is required" }, { status: 400 });
{ error: "Markdown string is required" }, }
{ status: 400 }
);
}
// Log raw markdown input before conversion // Log raw markdown input before conversion
// console.log(`\n${"=".repeat(80)}`); // console.log(`\n${"=".repeat(80)}`);
// console.log("RAW MARKDOWN INPUT (BEFORE CONVERSION):"); // console.log("RAW MARKDOWN INPUT (BEFORE CONVERSION):");
// console.log("=".repeat(80)); // console.log("=".repeat(80));
// console.log(markdown); // console.log(markdown);
// console.log(`${"=".repeat(80)}\n`); // console.log(`${"=".repeat(80)}\n`);
// Create server-side editor instance // Create server-side editor instance
const editor = ServerBlockNoteEditor.create(); const editor = ServerBlockNoteEditor.create();
// Convert markdown directly to BlockNote blocks // Convert markdown directly to BlockNote blocks
const blocks = await editor.tryParseMarkdownToBlocks(markdown); const blocks = await editor.tryParseMarkdownToBlocks(markdown);
if (!blocks || blocks.length === 0) { if (!blocks || blocks.length === 0) {
throw new Error("Markdown parsing returned no blocks"); throw new Error("Markdown parsing returned no blocks");
} }
return NextResponse.json({ blocknote_document: blocks }); return NextResponse.json({ blocknote_document: blocks });
} catch (error: any) { } catch (error: any) {
console.error("Failed to convert markdown to BlockNote:", error); console.error("Failed to convert markdown to BlockNote:", error);
return NextResponse.json( return NextResponse.json(
{ {
error: "Failed to convert markdown to BlockNote blocks", error: "Failed to convert markdown to BlockNote blocks",
details: error.message details: error.message,
}, },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -2,30 +2,27 @@ import { ServerBlockNoteEditor } from "@blocknote/server-util";
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { blocknote_document } = await request.json(); const { blocknote_document } = await request.json();
if (!blocknote_document || !Array.isArray(blocknote_document)) { if (!blocknote_document || !Array.isArray(blocknote_document)) {
return NextResponse.json( return NextResponse.json({ error: "BlockNote document array is required" }, { status: 400 });
{ error: "BlockNote document array is required" }, }
{ status: 400 }
);
}
// Create server-side editor instance // Create server-side editor instance
const editor = ServerBlockNoteEditor.create(); const editor = ServerBlockNoteEditor.create();
// Convert BlockNote blocks to markdown // Convert BlockNote blocks to markdown
const markdown = await editor.blocksToMarkdownLossy(blocknote_document); const markdown = await editor.blocksToMarkdownLossy(blocknote_document);
return NextResponse.json({ return NextResponse.json({
markdown markdown,
}); });
} catch (error) { } catch (error) {
console.error("Failed to convert BlockNote to markdown:", error); console.error("Failed to convert BlockNote to markdown:", error);
return NextResponse.json( return NextResponse.json(
{ error: "Failed to convert BlockNote blocks to markdown" }, { error: "Failed to convert BlockNote blocks to markdown" },
{ status: 500 } { status: 500 }
); );
} }
} }

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { MoreHorizontal, Pencil, FileText, Trash2 } from "lucide-react"; import { FileText, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";

View file

@ -1,9 +1,9 @@
"use client"; "use client";
import { AlertCircle, FileText, Loader2, Save, X } from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { motion } from "motion/react";
import { Loader2, Save, X, FileText, AlertCircle } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor"; import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -12,286 +12,281 @@ import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface EditorContent { interface EditorContent {
document_id: number; document_id: number;
title: string; title: string;
blocknote_document: any; blocknote_document: any;
last_edited_at: string | null; last_edited_at: string | null;
} }
export default function EditorPage() { export default function EditorPage() {
const params = useParams(); const params = useParams();
const router = useRouter(); const router = useRouter();
const documentId = params.documentId as string; const documentId = params.documentId as string;
const [document, setDocument] = useState<EditorContent | null>(null); const [document, setDocument] = useState<EditorContent | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [editorContent, setEditorContent] = useState<any>(null); const [editorContent, setEditorContent] = useState<any>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// Get auth token // Get auth token
const token = typeof window !== "undefined" const token =
? localStorage.getItem("surfsense_bearer_token") typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") : null;
: null;
// Fetch document content - DIRECT CALL TO FASTAPI // Fetch document content - DIRECT CALL TO FASTAPI
useEffect(() => { useEffect(() => {
async function fetchDocument() { async function fetchDocument() {
if (!token) { if (!token) {
console.error("No auth token found"); console.error("No auth token found");
setError("Please login to access the editor"); setError("Please login to access the editor");
setLoading(false); setLoading(false);
return; return;
} }
try { try {
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/editor-content`, `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/editor-content`,
{ {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
} }
); );
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: "Failed to fetch document" })); const errorData = await response
throw new Error(errorData.detail || "Failed to fetch document"); .json()
} .catch(() => ({ detail: "Failed to fetch document" }));
throw new Error(errorData.detail || "Failed to fetch document");
}
const data = await response.json(); const data = await response.json();
// Check if blocknote_document exists // Check if blocknote_document exists
if (!data.blocknote_document) { if (!data.blocknote_document) {
setError("This document does not have BlockNote content. Please re-upload the document to enable editing."); setError(
setLoading(false); "This document does not have BlockNote content. Please re-upload the document to enable editing."
return; );
} setLoading(false);
return;
}
setDocument(data); setDocument(data);
setEditorContent(data.blocknote_document); setEditorContent(data.blocknote_document);
setError(null); setError(null);
} catch (error) { } catch (error) {
console.error("Error fetching document:", error); console.error("Error fetching document:", error);
setError(error instanceof Error ? error.message : "Failed to fetch document. Please try again."); setError(
} finally { error instanceof Error ? error.message : "Failed to fetch document. Please try again."
setLoading(false); );
} } finally {
} setLoading(false);
}
}
if (documentId && token) { if (documentId && token) {
fetchDocument(); fetchDocument();
} }
}, [documentId, token]); }, [documentId, token]);
// Track changes to mark as unsaved // Track changes to mark as unsaved
useEffect(() => { useEffect(() => {
if (editorContent && document) { if (editorContent && document) {
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
} }
}, [editorContent, document]); }, [editorContent, document]);
// Auto-save every 30 seconds - DIRECT CALL TO FASTAPI // Auto-save every 30 seconds - DIRECT CALL TO FASTAPI
useEffect(() => { useEffect(() => {
if (!editorContent || !token || !hasUnsavedChanges) return; if (!editorContent || !token || !hasUnsavedChanges) return;
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/blocknote-content`, `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/blocknote-content`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ blocknote_document: editorContent }), body: JSON.stringify({ blocknote_document: editorContent }),
} }
); );
if (response.ok) { if (response.ok) {
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
toast.success("Auto-saved", { duration: 2000 }); toast.success("Auto-saved", { duration: 2000 });
} }
} catch (error) { } catch (error) {
console.error("Auto-save failed:", error); console.error("Auto-save failed:", error);
} }
}, 30000); // 30 seconds }, 30000); // 30 seconds
return () => clearInterval(interval); return () => clearInterval(interval);
}, [editorContent, documentId, token, hasUnsavedChanges]); }, [editorContent, documentId, token, hasUnsavedChanges]);
// Save and exit - DIRECT CALL TO FASTAPI // Save and exit - DIRECT CALL TO FASTAPI
const handleSave = async () => { const handleSave = async () => {
if (!token) { if (!token) {
toast.error("Please login to save"); toast.error("Please login to save");
return; return;
} }
if (!editorContent) { if (!editorContent) {
toast.error("No content to save"); toast.error("No content to save");
return; return;
} }
setSaving(true); setSaving(true);
try { try {
// Save blocknote_document to database (without finalizing/reindexing) // Save blocknote_document to database (without finalizing/reindexing)
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/blocknote-content`, `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/blocknote-content`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ blocknote_document: editorContent }), body: JSON.stringify({ blocknote_document: editorContent }),
} }
); );
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: "Failed to save document" })); const errorData = await response
throw new Error(errorData.detail || "Failed to save document"); .json()
} .catch(() => ({ detail: "Failed to save document" }));
throw new Error(errorData.detail || "Failed to save document");
}
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
toast.success("Document saved successfully"); toast.success("Document saved successfully");
// Small delay before redirect to show success message // Small delay before redirect to show success message
setTimeout(() => { setTimeout(() => {
router.push(`/dashboard/${params.search_space_id}/documents`); router.push(`/dashboard/${params.search_space_id}/documents`);
}, 500); }, 500);
} catch (error) { } catch (error) {
console.error("Error saving document:", error); console.error("Error saving document:", error);
toast.error(error instanceof Error ? error.message : "Failed to save document. Please try again."); toast.error(
} finally { error instanceof Error ? error.message : "Failed to save document. Please try again."
setSaving(false); );
} } finally {
}; setSaving(false);
}
};
const handleCancel = () => { const handleCancel = () => {
if (hasUnsavedChanges) { if (hasUnsavedChanges) {
if (confirm("You have unsaved changes. Are you sure you want to leave?")) { if (confirm("You have unsaved changes. Are you sure you want to leave?")) {
router.back(); router.back();
} }
} else { } else {
router.back(); router.back();
} }
}; };
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center min-h-[400px] p-6"> <div className="flex items-center justify-center min-h-[400px] p-6">
<Card className="w-full max-w-md"> <Card className="w-full max-w-md">
<CardContent className="flex flex-col items-center justify-center py-12"> <CardContent className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-12 w-12 text-primary animate-spin mb-4" /> <Loader2 className="h-12 w-12 text-primary animate-spin mb-4" />
<p className="text-muted-foreground">Loading editor...</p> <p className="text-muted-foreground">Loading editor...</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); );
} }
if (error) { if (error) {
return ( return (
<div className="flex items-center justify-center min-h-[400px] p-6"> <div className="flex items-center justify-center min-h-[400px] p-6">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md" className="w-full max-w-md"
> >
<Card className="border-destructive/50"> <Card className="border-destructive/50">
<CardHeader> <CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" /> <AlertCircle className="h-5 w-5 text-destructive" />
<CardTitle className="text-destructive">Error</CardTitle> <CardTitle className="text-destructive">Error</CardTitle>
</div> </div>
<CardDescription>{error}</CardDescription> <CardDescription>{error}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Button onClick={() => router.back()} variant="outline" className="w-full"> <Button onClick={() => router.back()} variant="outline" className="w-full">
<X className="mr-2 h-4 w-4" /> <X className="mr-2 h-4 w-4" />
Go Back Go Back
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
</motion.div> </motion.div>
</div> </div>
); );
} }
if (!document) { if (!document) {
return ( return (
<div className="flex items-center justify-center min-h-[400px] p-6"> <div className="flex items-center justify-center min-h-[400px] p-6">
<Card className="w-full max-w-md"> <Card className="w-full max-w-md">
<CardContent className="flex flex-col items-center justify-center py-12"> <CardContent className="flex flex-col items-center justify-center py-12">
<FileText className="h-12 w-12 text-muted-foreground mb-4" /> <FileText className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">Document not found</p> <p className="text-muted-foreground">Document not found</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); );
} }
return ( return (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
className="flex flex-col h-full w-full" className="flex flex-col h-full w-full"
> >
{/* Toolbar */} {/* Toolbar */}
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-4 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-6"> <div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-4 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-6">
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-3 flex-1 min-w-0">
<FileText className="h-5 w-5 text-muted-foreground shrink-0" /> <FileText className="h-5 w-5 text-muted-foreground shrink-0" />
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
<h1 className="text-lg font-semibold truncate">{document.title}</h1> <h1 className="text-lg font-semibold truncate">{document.title}</h1>
{hasUnsavedChanges && ( {hasUnsavedChanges && <p className="text-xs text-muted-foreground">Unsaved changes</p>}
<p className="text-xs text-muted-foreground">Unsaved changes</p> </div>
)} </div>
</div> <Separator orientation="vertical" className="h-6" />
</div> <div className="flex items-center gap-2">
<Separator orientation="vertical" className="h-6" /> <Button variant="outline" onClick={handleCancel} disabled={saving} className="gap-2">
<div className="flex items-center gap-2"> <X className="h-4 w-4" />
<Button Cancel
variant="outline" </Button>
onClick={handleCancel} <Button onClick={handleSave} disabled={saving} className="gap-2">
disabled={saving} {saving ? (
className="gap-2" <>
> <Loader2 className="h-4 w-4 animate-spin" />
<X className="h-4 w-4" /> Saving...
Cancel </>
</Button> ) : (
<Button <>
onClick={handleSave} <Save className="h-4 w-4" />
disabled={saving} Save & Exit
className="gap-2" </>
> )}
{saving ? ( </Button>
<> </div>
<Loader2 className="h-4 w-4 animate-spin" /> </div>
Saving...
</>
) : (
<>
<Save className="h-4 w-4" />
Save & Exit
</>
)}
</Button>
</div>
</div>
{/* Editor Container */} {/* Editor Container */}
<div className="flex-1 overflow-hidden relative"> <div className="flex-1 overflow-hidden relative">
<div className="h-full w-full overflow-auto p-6"> <div className="h-full w-full overflow-auto p-6">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<BlockNoteEditor <BlockNoteEditor initialContent={editorContent} onChange={setEditorContent} />
initialContent={editorContent} </div>
onChange={setEditorContent} </div>
/> </div>
</div> </motion.div>
</div> );
</div>
</motion.div>
);
} }

View file

@ -1,71 +1,68 @@
"use client"; "use client";
import { useEffect, useRef, useMemo } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useEffect, useMemo, useRef } from "react";
import "@blocknote/core/fonts/inter.css"; import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css"; import "@blocknote/mantine/style.css";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine"; import { BlockNoteView } from "@blocknote/mantine";
import { useCreateBlockNote } from "@blocknote/react";
interface BlockNoteEditorProps { interface BlockNoteEditorProps {
initialContent?: any; initialContent?: any;
onChange?: (content: any) => void; onChange?: (content: any) => void;
} }
export default function BlockNoteEditor({ export default function BlockNoteEditor({ initialContent, onChange }: BlockNoteEditorProps) {
initialContent, const { resolvedTheme } = useTheme();
onChange,
}: BlockNoteEditorProps) {
const { resolvedTheme } = useTheme();
// Track the initial content to prevent re-initialization // Track the initial content to prevent re-initialization
const initialContentRef = useRef<any>(null); const initialContentRef = useRef<any>(null);
const isInitializedRef = useRef(false); const isInitializedRef = useRef(false);
// Creates a new editor instance - only use initialContent on first render // Creates a new editor instance - only use initialContent on first render
const editor = useCreateBlockNote({ const editor = useCreateBlockNote({
initialContent: initialContentRef.current === null ? (initialContent || undefined) : undefined, initialContent: initialContentRef.current === null ? initialContent || undefined : undefined,
}); });
// Store initial content on first render only // Store initial content on first render only
useEffect(() => { useEffect(() => {
if (initialContent && initialContentRef.current === null) { if (initialContent && initialContentRef.current === null) {
initialContentRef.current = initialContent; initialContentRef.current = initialContent;
isInitializedRef.current = true; isInitializedRef.current = true;
} }
}, [initialContent]); }, [initialContent]);
// Call onChange when document changes (but don't update from props) // Call onChange when document changes (but don't update from props)
useEffect(() => { useEffect(() => {
if (!onChange || !editor || !isInitializedRef.current) return; if (!onChange || !editor || !isInitializedRef.current) return;
const handleChange = () => { const handleChange = () => {
onChange(editor.document); onChange(editor.document);
}; };
// Subscribe to document changes // Subscribe to document changes
const unsubscribe = editor.onChange(handleChange); const unsubscribe = editor.onChange(handleChange);
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [editor, onChange]); }, [editor, onChange]);
// Determine theme for BlockNote with custom dark mode background // Determine theme for BlockNote with custom dark mode background
const blockNoteTheme = useMemo(() => { const blockNoteTheme = useMemo(() => {
if (resolvedTheme === "dark") { if (resolvedTheme === "dark") {
// Custom dark theme - only override editor background, let BlockNote handle the rest // Custom dark theme - only override editor background, let BlockNote handle the rest
return { return {
colors: { colors: {
editor: { editor: {
background: "#0A0A0A", // Custom dark background background: "#0A0A0A", // Custom dark background
}, },
}, },
}; };
} }
return "light" as const; return "light" as const;
}, [resolvedTheme]); }, [resolvedTheme]);
// Renders the editor instance // Renders the editor instance
return <BlockNoteView editor={editor} theme={blockNoteTheme} />; return <BlockNoteView editor={editor} theme={blockNoteTheme} />;
} }

View file

@ -3,7 +3,4 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
// Dynamically import BlockNote editor with SSR disabled // Dynamically import BlockNote editor with SSR disabled
export const BlockNoteEditor = dynamic( export const BlockNoteEditor = dynamic(() => import("./BlockNoteEditor"), { ssr: false });
() => import("./BlockNoteEditor"),
{ ssr: false }
);

View file

@ -41,9 +41,8 @@ export function DashboardBreadcrumb() {
useEffect(() => { useEffect(() => {
if (segments[2] === "editor" && segments[3] && searchSpaceId) { if (segments[2] === "editor" && segments[3] && searchSpaceId) {
const documentId = segments[3]; const documentId = segments[3];
const token = typeof window !== "undefined" const token =
? localStorage.getItem("surfsense_bearer_token") typeof window !== "undefined" ? localStorage.getItem("surfsense_bearer_token") : null;
: null;
if (token) { if (token) {
fetch( fetch(

View file

@ -24,18 +24,13 @@ const nextConfig: NextConfig = {
], ],
}, },
// Mark BlockNote server packages as external // Mark BlockNote server packages as external
serverExternalPackages: [ serverExternalPackages: ["@blocknote/server-util"],
'@blocknote/server-util',
],
// Configure webpack to handle blocknote packages // Configure webpack to handle blocknote packages
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
if (isServer) { if (isServer) {
// Don't bundle these packages on the server // Don't bundle these packages on the server
config.externals = [ config.externals = [...(config.externals || []), "@blocknote/server-util"];
...(config.externals || []),
'@blocknote/server-util',
];
} }
return config; return config;
}, },