mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-22 21:28:12 +02:00
introduced blocknote editor
This commit is contained in:
parent
70f3381d7e
commit
e68286f22e
23 changed files with 2158 additions and 14 deletions
43
surfsense_web/app/api/convert-to-blocknote/route.ts
Normal file
43
surfsense_web/app/api/convert-to-blocknote/route.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { ServerBlockNoteEditor } from "@blocknote/server-util";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { markdown } = await request.json();
|
||||
|
||||
if (!markdown || typeof markdown !== "string") {
|
||||
return NextResponse.json(
|
||||
{ error: "Markdown string is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Log raw markdown input before conversion
|
||||
// console.log(`\n${"=".repeat(80)}`);
|
||||
// console.log("RAW MARKDOWN INPUT (BEFORE CONVERSION):");
|
||||
// console.log("=".repeat(80));
|
||||
// console.log(markdown);
|
||||
// console.log(`${"=".repeat(80)}\n`);
|
||||
|
||||
// Create server-side editor instance
|
||||
const editor = ServerBlockNoteEditor.create();
|
||||
|
||||
// Convert markdown directly to BlockNote blocks
|
||||
const blocks = await editor.tryParseMarkdownToBlocks(markdown);
|
||||
|
||||
if (!blocks || blocks.length === 0) {
|
||||
throw new Error("Markdown parsing returned no blocks");
|
||||
}
|
||||
|
||||
return NextResponse.json({ blocknote_document: blocks });
|
||||
} catch (error: any) {
|
||||
console.error("Failed to convert markdown to BlockNote:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Failed to convert markdown to BlockNote blocks",
|
||||
details: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
31
surfsense_web/app/api/convert-to-markdown/route.ts
Normal file
31
surfsense_web/app/api/convert-to-markdown/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { ServerBlockNoteEditor } from "@blocknote/server-util";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { blocknote_document } = await request.json();
|
||||
|
||||
if (!blocknote_document || !Array.isArray(blocknote_document)) {
|
||||
return NextResponse.json(
|
||||
{ error: "BlockNote document array is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create server-side editor instance
|
||||
const editor = ServerBlockNoteEditor.create();
|
||||
|
||||
// Convert BlockNote blocks to markdown
|
||||
const markdown = await editor.blocksToMarkdownLossy(blocknote_document);
|
||||
|
||||
return NextResponse.json({
|
||||
markdown
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to convert BlockNote to markdown:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to convert BlockNote blocks to markdown" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -309,6 +309,7 @@ export function DocumentsTableShell({
|
|||
refreshDocuments={async () => {
|
||||
await onRefresh();
|
||||
}}
|
||||
searchSpaceId={searchSpaceId as string}
|
||||
/>
|
||||
</TableCell>
|
||||
</motion.tr>
|
||||
|
|
@ -340,6 +341,7 @@ export function DocumentsTableShell({
|
|||
refreshDocuments={async () => {
|
||||
await onRefresh();
|
||||
}}
|
||||
searchSpaceId={searchSpaceId as string}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { MoreHorizontal, Pencil, FileText, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
||||
|
|
@ -28,13 +29,16 @@ export function RowActions({
|
|||
document,
|
||||
deleteDocument,
|
||||
refreshDocuments,
|
||||
searchSpaceId,
|
||||
}: {
|
||||
document: Document;
|
||||
deleteDocument: (id: number) => Promise<boolean>;
|
||||
refreshDocuments: () => Promise<void>;
|
||||
searchSpaceId: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
|
|
@ -52,6 +56,10 @@ export function RowActions({
|
|||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/dashboard/${searchSpaceId}/editor/${document.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
|
|
@ -62,11 +70,17 @@ export function RowActions({
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
<Pencil className="mr-0 h-4 w-4" />
|
||||
Edit Document
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<JsonMetadataViewer
|
||||
title={document.title}
|
||||
metadata={document.document_metadata}
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<FileText className="mr-0 h-4 w-4" />
|
||||
View Metadata
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
|
|
@ -81,6 +95,7 @@ export function RowActions({
|
|||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-0 h-4 w-4 text-destructive" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
"use client";
|
||||
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor";
|
||||
|
||||
interface EditorContent {
|
||||
document_id: number;
|
||||
title: string;
|
||||
blocknote_document: any;
|
||||
last_edited_at: string | null;
|
||||
}
|
||||
|
||||
export default function EditorPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const documentId = params.documentId as string;
|
||||
|
||||
const [document, setDocument] = useState<EditorContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [editorContent, setEditorContent] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Get auth token
|
||||
const token = typeof window !== "undefined"
|
||||
? localStorage.getItem("surfsense_bearer_token")
|
||||
: null;
|
||||
|
||||
// Fetch document content - DIRECT CALL TO FASTAPI
|
||||
useEffect(() => {
|
||||
async function fetchDocument() {
|
||||
if (!token) {
|
||||
console.error("No auth token found");
|
||||
setError("Please login to access the editor");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/editor-content`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: "Failed to fetch document" }));
|
||||
throw new Error(errorData.detail || "Failed to fetch document");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check if blocknote_document exists
|
||||
if (!data.blocknote_document) {
|
||||
setError("This document does not have BlockNote content. Please re-upload the document to enable editing.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDocument(data);
|
||||
setEditorContent(data.blocknote_document);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching document:", error);
|
||||
setError(error instanceof Error ? error.message : "Failed to fetch document. Please try again.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (documentId && token) {
|
||||
fetchDocument();
|
||||
}
|
||||
}, [documentId, token]);
|
||||
|
||||
// Auto-save every 30 seconds - DIRECT CALL TO FASTAPI
|
||||
useEffect(() => {
|
||||
if (!editorContent || !token) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/blocknote-content`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ blocknote_document: editorContent }),
|
||||
}
|
||||
);
|
||||
console.log("Auto-saved");
|
||||
} catch (error) {
|
||||
console.error("Auto-save failed:", error);
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [editorContent, documentId, token]);
|
||||
|
||||
// Save and exit - DIRECT CALL TO FASTAPI
|
||||
const handleSave = async () => {
|
||||
if (!token) {
|
||||
alert("Please login to save");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editorContent) {
|
||||
alert("No content to save");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
// Save blocknote_document to database (without finalizing/reindexing)
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/blocknote-content`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
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");
|
||||
}
|
||||
|
||||
// Redirect back to documents list
|
||||
router.push(`/dashboard/${params.search_space_id}/documents`);
|
||||
} catch (error) {
|
||||
console.error("Error saving document:", error);
|
||||
alert(error instanceof Error ? error.message : "Failed to save document. Please try again.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading editor...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
// <div className="h-screen flex items-center justify-center">
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="max-w-md p-6 border border-red-300 rounded-lg bg-red-50">
|
||||
<h2 className="text-xl font-bold text-red-800 mb-2">Error</h2>
|
||||
<p className="text-red-700 mb-4">{error}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!document) {
|
||||
return <div>Document not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
// <div className="h-screen flex flex-col">
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Toolbar */}
|
||||
<div className="border-b p-4 flex justify-between items-center">
|
||||
<h1 className="text-xl font-bold">{document.title}</h1>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
className="px-4 py-2 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving..." : "Save & Exit"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor - Now using dynamic import */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<BlockNoteEditor
|
||||
initialContent={editorContent}
|
||||
onChange={setEditorContent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
surfsense_web/components/BlockNoteEditor.tsx
Normal file
53
surfsense_web/components/BlockNoteEditor.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import "@blocknote/core/fonts/inter.css";
|
||||
import "@blocknote/mantine/style.css";
|
||||
import { useCreateBlockNote } from "@blocknote/react";
|
||||
import { BlockNoteView } from "@blocknote/mantine";
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
initialContent?: any;
|
||||
onChange?: (content: any) => void;
|
||||
}
|
||||
|
||||
export default function BlockNoteEditor({
|
||||
initialContent,
|
||||
onChange,
|
||||
}: BlockNoteEditorProps) {
|
||||
// Track the initial content to prevent re-initialization
|
||||
const initialContentRef = useRef<any>(null);
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
// Creates a new editor instance - only use initialContent on first render
|
||||
const editor = useCreateBlockNote({
|
||||
initialContent: initialContentRef.current === null ? (initialContent || undefined) : undefined,
|
||||
});
|
||||
|
||||
// Store initial content on first render only
|
||||
useEffect(() => {
|
||||
if (initialContent && initialContentRef.current === null) {
|
||||
initialContentRef.current = initialContent;
|
||||
isInitializedRef.current = true;
|
||||
}
|
||||
}, [initialContent]);
|
||||
|
||||
// Call onChange when document changes (but don't update from props)
|
||||
useEffect(() => {
|
||||
if (!onChange || !editor || !isInitializedRef.current) return;
|
||||
|
||||
const handleChange = () => {
|
||||
onChange(editor.document);
|
||||
};
|
||||
|
||||
// Subscribe to document changes
|
||||
const unsubscribe = editor.onChange(handleChange);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [editor, onChange]);
|
||||
|
||||
// Renders the editor instance
|
||||
return <BlockNoteView editor={editor} />;
|
||||
}
|
||||
9
surfsense_web/components/DynamicBlockNoteEditor.tsx
Normal file
9
surfsense_web/components/DynamicBlockNoteEditor.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Dynamically import BlockNote editor with SSR disabled
|
||||
export const BlockNoteEditor = dynamic(
|
||||
() => import("./BlockNoteEditor"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import { useAtomValue } from "jotai";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { activeChatAtom } from "@/atoms/chats/chat-query.atoms";
|
||||
import {
|
||||
Breadcrumb,
|
||||
|
|
@ -34,6 +34,42 @@ export function DashboardBreadcrumb() {
|
|||
autoFetch: !!searchSpaceId,
|
||||
});
|
||||
|
||||
// State to store document title for editor breadcrumb
|
||||
const [documentTitle, setDocumentTitle] = useState<string | null>(null);
|
||||
|
||||
// Fetch document title when on editor page
|
||||
useEffect(() => {
|
||||
if (segments[2] === "editor" && segments[3] && searchSpaceId) {
|
||||
const documentId = segments[3];
|
||||
const token = typeof window !== "undefined"
|
||||
? localStorage.getItem("surfsense_bearer_token")
|
||||
: null;
|
||||
|
||||
if (token) {
|
||||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/${documentId}/editor-content`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.title) {
|
||||
setDocumentTitle(data.title);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// If fetch fails, just use the document ID
|
||||
setDocumentTitle(null);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setDocumentTitle(null);
|
||||
}
|
||||
}, [segments, searchSpaceId]);
|
||||
|
||||
// Parse the pathname to create breadcrumb items
|
||||
const generateBreadcrumbs = (path: string): BreadcrumbItemInterface[] => {
|
||||
const segments = path.split("/").filter(Boolean);
|
||||
|
|
@ -66,6 +102,7 @@ export function DashboardBreadcrumb() {
|
|||
logs: t("logs"),
|
||||
chats: t("chats"),
|
||||
settings: t("settings"),
|
||||
editor: t("editor"),
|
||||
};
|
||||
|
||||
sectionLabel = sectionLabels[section] || sectionLabel;
|
||||
|
|
@ -73,7 +110,21 @@ export function DashboardBreadcrumb() {
|
|||
// Handle sub-sections
|
||||
if (segments[3]) {
|
||||
const subSection = segments[3];
|
||||
let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1);
|
||||
|
||||
// Handle editor sub-sections (document ID)
|
||||
if (section === "editor") {
|
||||
const documentLabel = documentTitle || subSection;
|
||||
breadcrumbs.push({
|
||||
label: t("documents"),
|
||||
href: `/dashboard/${segments[1]}/documents`,
|
||||
});
|
||||
breadcrumbs.push({
|
||||
label: sectionLabel,
|
||||
href: `/dashboard/${segments[1]}/documents`,
|
||||
});
|
||||
breadcrumbs.push({ label: documentLabel });
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
// Handle sources sub-sections
|
||||
if (section === "sources") {
|
||||
|
|
@ -81,7 +132,7 @@ export function DashboardBreadcrumb() {
|
|||
add: "Add Sources",
|
||||
};
|
||||
|
||||
const sourceLabel = sourceLabels[subSection] || subSectionLabel;
|
||||
const sourceLabel = sourceLabels[subSection] || subSection;
|
||||
breadcrumbs.push({
|
||||
label: "Sources",
|
||||
href: `/dashboard/${segments[1]}/sources`,
|
||||
|
|
@ -98,7 +149,7 @@ export function DashboardBreadcrumb() {
|
|||
webpage: t("add_webpages"),
|
||||
};
|
||||
|
||||
const documentLabel = documentLabels[subSection] || subSectionLabel;
|
||||
const documentLabel = documentLabels[subSection] || subSection;
|
||||
breadcrumbs.push({
|
||||
label: t("documents"),
|
||||
href: `/dashboard/${segments[1]}/documents`,
|
||||
|
|
@ -158,7 +209,7 @@ export function DashboardBreadcrumb() {
|
|||
manage: t("manage_connectors"),
|
||||
};
|
||||
|
||||
const connectorLabel = connectorLabels[subSection] || subSectionLabel;
|
||||
const connectorLabel = connectorLabels[subSection] || subSection;
|
||||
breadcrumbs.push({
|
||||
label: t("connectors"),
|
||||
href: `/dashboard/${segments[1]}/connectors`,
|
||||
|
|
@ -168,6 +219,7 @@ export function DashboardBreadcrumb() {
|
|||
}
|
||||
|
||||
// Handle other sub-sections
|
||||
let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1);
|
||||
const subSectionLabels: Record<string, string> = {
|
||||
upload: t("upload_documents"),
|
||||
youtube: t("add_youtube"),
|
||||
|
|
|
|||
|
|
@ -615,6 +615,7 @@
|
|||
"documents": "Documents",
|
||||
"connectors": "Connectors",
|
||||
"podcasts": "Podcasts",
|
||||
"editor": "Editor",
|
||||
"logs": "Logs",
|
||||
"chats": "Chats",
|
||||
"settings": "Settings",
|
||||
|
|
|
|||
|
|
@ -615,6 +615,7 @@
|
|||
"documents": "文档",
|
||||
"connectors": "连接器",
|
||||
"podcasts": "播客",
|
||||
"editor": "编辑器",
|
||||
"logs": "日志",
|
||||
"chats": "聊天",
|
||||
"settings": "设置",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const withNextIntl = createNextIntlPlugin("./i18n/request.ts");
|
|||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
// Disable StrictMode for BlockNote compatibility with React 19/Next 15
|
||||
reactStrictMode: false,
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
|
@ -21,6 +23,22 @@ const nextConfig: NextConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
// Mark BlockNote server packages as external
|
||||
serverExternalPackages: [
|
||||
'@blocknote/server-util',
|
||||
],
|
||||
|
||||
// Configure webpack to handle blocknote packages
|
||||
webpack: (config, { isServer }) => {
|
||||
if (isServer) {
|
||||
// Don't bundle these packages on the server
|
||||
config.externals = [
|
||||
...(config.externals || []),
|
||||
'@blocknote/server-util',
|
||||
];
|
||||
}
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
// Wrap the config with MDX and next-intl plugins
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "^1.2.12",
|
||||
"@blocknote/core": "^0.42.3",
|
||||
"@blocknote/mantine": "^0.42.3",
|
||||
"@blocknote/react": "^0.42.3",
|
||||
"@blocknote/server-util": "^0.42.3",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@llamaindex/chat-ui": "^0.5.17",
|
||||
"@next/third-parties": "^15.5.6",
|
||||
|
|
|
|||
1316
surfsense_web/pnpm-lock.yaml
generated
1316
surfsense_web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue