mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
code quality issues fixed
This commit is contained in:
parent
abbaa848f3
commit
3fac196c35
17 changed files with 495 additions and 493 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue