mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
Merge commit '77688ac80c' into dev
This commit is contained in:
commit
4b8a2f9726
3 changed files with 117 additions and 35 deletions
|
|
@ -38,7 +38,8 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
EDITOR_PLATE_MAX_BYTES = 5 * 1024 * 1024
|
EDITOR_PLATE_MAX_BYTES = 1 * 1024 * 1024
|
||||||
|
EDITOR_PLATE_MAX_LINES = 5000
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search-spaces/{search_space_id}/documents/{document_id}/editor-content")
|
@router.get("/search-spaces/{search_space_id}/documents/{document_id}/editor-content")
|
||||||
|
|
@ -83,16 +84,23 @@ async def get_editor_content(
|
||||||
|
|
||||||
def _build_response(md: str) -> dict:
|
def _build_response(md: str) -> dict:
|
||||||
size_bytes = len(md.encode("utf-8"))
|
size_bytes = len(md.encode("utf-8"))
|
||||||
viewer_mode = "monaco" if size_bytes > EDITOR_PLATE_MAX_BYTES else "plate"
|
line_count = md.count("\n") + 1
|
||||||
|
too_large = (
|
||||||
|
size_bytes > EDITOR_PLATE_MAX_BYTES
|
||||||
|
or line_count > EDITOR_PLATE_MAX_LINES
|
||||||
|
)
|
||||||
|
viewer_mode = "monaco" if too_large else "plate"
|
||||||
return {
|
return {
|
||||||
"document_id": document.id,
|
"document_id": document.id,
|
||||||
"title": document.title,
|
"title": document.title,
|
||||||
"document_type": document.document_type.value,
|
"document_type": document.document_type.value,
|
||||||
"source_markdown": md,
|
"source_markdown": md,
|
||||||
"content_size_bytes": size_bytes,
|
"content_size_bytes": size_bytes,
|
||||||
|
"line_count": line_count,
|
||||||
"chunk_count": chunk_count,
|
"chunk_count": chunk_count,
|
||||||
"viewer_mode": viewer_mode,
|
"viewer_mode": viewer_mode,
|
||||||
"editor_plate_max_bytes": EDITOR_PLATE_MAX_BYTES,
|
"editor_plate_max_bytes": EDITOR_PLATE_MAX_BYTES,
|
||||||
|
"editor_plate_max_lines": EDITOR_PLATE_MAX_LINES,
|
||||||
"updated_at": document.updated_at.isoformat()
|
"updated_at": document.updated_at.isoformat()
|
||||||
if document.updated_at
|
if document.updated_at
|
||||||
else None,
|
else None,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { toast } from "sonner";
|
||||||
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||||
import { DownloadOriginalButton } from "@/components/documents/download-original-button";
|
import { DownloadOriginalButton } from "@/components/documents/download-original-button";
|
||||||
import { VersionHistoryButton } from "@/components/documents/version-history";
|
import { VersionHistoryButton } from "@/components/documents/version-history";
|
||||||
|
import { PlateErrorBoundary } from "@/components/editor/plate-error-boundary";
|
||||||
import { SourceCodeEditor } from "@/components/editor/source-code-editor";
|
import { SourceCodeEditor } from "@/components/editor/source-code-editor";
|
||||||
import {
|
import {
|
||||||
fetchMemoryEditorDocument,
|
fetchMemoryEditorDocument,
|
||||||
|
|
@ -41,7 +42,8 @@ const PlateEditor = dynamic(
|
||||||
{ ssr: false, loading: () => <EditorPanelSkeleton /> }
|
{ ssr: false, loading: () => <EditorPanelSkeleton /> }
|
||||||
);
|
);
|
||||||
|
|
||||||
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
const LARGE_DOCUMENT_THRESHOLD = 1 * 1024 * 1024; // 1MB, matches backend
|
||||||
|
const LARGE_DOCUMENT_LINE_THRESHOLD = 5000;
|
||||||
|
|
||||||
interface EditorContent {
|
interface EditorContent {
|
||||||
document_id: number;
|
document_id: number;
|
||||||
|
|
@ -49,9 +51,11 @@ interface EditorContent {
|
||||||
document_type?: string;
|
document_type?: string;
|
||||||
source_markdown: string;
|
source_markdown: string;
|
||||||
content_size_bytes?: number;
|
content_size_bytes?: number;
|
||||||
|
line_count?: number;
|
||||||
chunk_count?: number;
|
chunk_count?: number;
|
||||||
viewer_mode?: ViewerMode;
|
viewer_mode?: ViewerMode;
|
||||||
editor_plate_max_bytes?: number;
|
editor_plate_max_bytes?: number;
|
||||||
|
editor_plate_max_lines?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]);
|
const EDITABLE_DOCUMENT_TYPES = new Set(["FILE", "NOTE"]);
|
||||||
|
|
@ -118,6 +122,15 @@ function getUtf8ByteSize(value: string): number {
|
||||||
return new TextEncoder().encode(value).byteLength;
|
return new TextEncoder().encode(value).byteLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countLines(value: string): number {
|
||||||
|
if (!value) return 0;
|
||||||
|
let count = 1;
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
if (value.charCodeAt(i) === 10) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
if (bytes >= 1024 * 1024) {
|
if (bytes >= 1024 * 1024) {
|
||||||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
||||||
|
|
@ -184,10 +197,17 @@ export function EditorPanelContent({
|
||||||
);
|
);
|
||||||
|
|
||||||
const plateMaxBytes = editorDoc?.editor_plate_max_bytes ?? LARGE_DOCUMENT_THRESHOLD;
|
const plateMaxBytes = editorDoc?.editor_plate_max_bytes ?? LARGE_DOCUMENT_THRESHOLD;
|
||||||
const isLargeDocument = (editorDoc?.content_size_bytes ?? 0) > plateMaxBytes;
|
const plateMaxLines = editorDoc?.editor_plate_max_lines ?? LARGE_DOCUMENT_LINE_THRESHOLD;
|
||||||
|
const docSizeBytes = editorDoc?.content_size_bytes ?? 0;
|
||||||
|
const docLineCount =
|
||||||
|
editorDoc?.line_count ??
|
||||||
|
(editorDoc?.source_markdown ? countLines(editorDoc.source_markdown) : 0);
|
||||||
|
const isLargeDocument = docSizeBytes > plateMaxBytes || docLineCount > plateMaxLines;
|
||||||
const viewerMode: ViewerMode = isMemoryMode
|
const viewerMode: ViewerMode = isMemoryMode
|
||||||
? "plate"
|
? "plate"
|
||||||
: (editorDoc?.viewer_mode ?? (isLargeDocument ? "monaco" : "plate"));
|
: editorDoc?.viewer_mode === "monaco" || isLargeDocument
|
||||||
|
? "monaco"
|
||||||
|
: "plate";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -423,7 +443,8 @@ export function EditorPanelContent({
|
||||||
setEditedMarkdown(null);
|
setEditedMarkdown(null);
|
||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
const savedSizeBytes = getUtf8ByteSize(markdownRef.current);
|
const savedSizeBytes = getUtf8ByteSize(markdownRef.current);
|
||||||
if (savedSizeBytes > plateMaxBytes) {
|
const savedLineCount = countLines(markdownRef.current);
|
||||||
|
if (savedSizeBytes > plateMaxBytes || savedLineCount > plateMaxLines) {
|
||||||
toast.success("Document saved. It will reopen in raw markdown mode.");
|
toast.success("Document saved. It will reopen in raw markdown mode.");
|
||||||
} else {
|
} else {
|
||||||
toast.success("Document saved! Reindexing in background...");
|
toast.success("Document saved! Reindexing in background...");
|
||||||
|
|
@ -449,6 +470,7 @@ export function EditorPanelContent({
|
||||||
memoryLimits,
|
memoryLimits,
|
||||||
memoryScope,
|
memoryScope,
|
||||||
plateMaxBytes,
|
plateMaxBytes,
|
||||||
|
plateMaxLines,
|
||||||
resolveLocalVirtualPath,
|
resolveLocalVirtualPath,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
]
|
]
|
||||||
|
|
@ -469,8 +491,12 @@ export function EditorPanelContent({
|
||||||
const localFileLanguage = inferMonacoLanguageFromPath(localFilePath);
|
const localFileLanguage = inferMonacoLanguageFromPath(localFilePath);
|
||||||
const activeMarkdown = editedMarkdown ?? editorDoc?.source_markdown ?? "";
|
const activeMarkdown = editedMarkdown ?? editorDoc?.source_markdown ?? "";
|
||||||
const activeMarkdownSizeBytes = useMemo(() => getUtf8ByteSize(activeMarkdown), [activeMarkdown]);
|
const activeMarkdownSizeBytes = useMemo(() => getUtf8ByteSize(activeMarkdown), [activeMarkdown]);
|
||||||
const isNearPlateLimit = activeMarkdownSizeBytes >= plateMaxBytes * 0.9;
|
const activeMarkdownLineCount = useMemo(() => countLines(activeMarkdown), [activeMarkdown]);
|
||||||
const isOverPlateLimit = activeMarkdownSizeBytes > plateMaxBytes;
|
const isNearPlateLimit =
|
||||||
|
activeMarkdownSizeBytes >= plateMaxBytes * 0.9 ||
|
||||||
|
activeMarkdownLineCount >= plateMaxLines * 0.9;
|
||||||
|
const isOverPlateLimit =
|
||||||
|
activeMarkdownSizeBytes > plateMaxBytes || activeMarkdownLineCount > plateMaxLines;
|
||||||
const showPlateSizeWarning =
|
const showPlateSizeWarning =
|
||||||
showEditingActions && !isMemoryMode && !isLocalFileMode && isNearPlateLimit;
|
showEditingActions && !isMemoryMode && !isLocalFileMode && isNearPlateLimit;
|
||||||
const memoryLimitState = isMemoryMode
|
const memoryLimitState = isMemoryMode
|
||||||
|
|
@ -483,6 +509,13 @@ export function EditorPanelContent({
|
||||||
? "text-orange-500"
|
? "text-orange-500"
|
||||||
: "text-muted-foreground";
|
: "text-muted-foreground";
|
||||||
const saveDisabled = saving || !hasUnsavedChanges || (memoryLimitState?.isOverLimit ?? false);
|
const saveDisabled = saving || !hasUnsavedChanges || (memoryLimitState?.isOverLimit ?? false);
|
||||||
|
const editorInstanceKey = `${
|
||||||
|
isMemoryMode
|
||||||
|
? `memory-${memoryScope ?? "user"}`
|
||||||
|
: isLocalFileMode
|
||||||
|
? (localFilePath ?? "local-file")
|
||||||
|
: documentId
|
||||||
|
}-${isEditing ? "editing" : "viewing"}`;
|
||||||
|
|
||||||
const handleCancelEditing = useCallback(() => {
|
const handleCancelEditing = useCallback(() => {
|
||||||
const savedContent = editorDoc?.source_markdown ?? "";
|
const savedContent = editorDoc?.source_markdown ?? "";
|
||||||
|
|
@ -529,7 +562,7 @@ export function EditorPanelContent({
|
||||||
<AlertDescription className="flex items-center justify-between gap-4">
|
<AlertDescription className="flex items-center justify-between gap-4">
|
||||||
<span>
|
<span>
|
||||||
This document is too large for the editor (
|
This document is too large for the editor (
|
||||||
{Math.round((editorDoc.content_size_bytes ?? 0) / 1024 / 1024)}MB,{" "}
|
{formatBytes(editorDoc.content_size_bytes ?? 0)}, {docLineCount.toLocaleString()} lines,{" "}
|
||||||
{editorDoc.chunk_count ?? 0} chunks). Showing raw markdown below.
|
{editorDoc.chunk_count ?? 0} chunks). Showing raw markdown below.
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -806,36 +839,43 @@ export function EditorPanelContent({
|
||||||
<FileText className="size-4" />
|
<FileText className="size-4" />
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{isOverPlateLimit
|
{isOverPlateLimit
|
||||||
? `This document is ${formatBytes(activeMarkdownSizeBytes)}, above the rich editor limit of ${formatBytes(plateMaxBytes)}. You can save, but it will reopen in raw markdown mode.`
|
? `This document is ${formatBytes(activeMarkdownSizeBytes)} and ${activeMarkdownLineCount.toLocaleString()} lines, above the rich editor limit of ${formatBytes(plateMaxBytes)} or ${plateMaxLines.toLocaleString()} lines. You can save, but it will reopen in raw markdown mode.`
|
||||||
: `This document is approaching the rich editor limit (${formatBytes(activeMarkdownSizeBytes)} of ${formatBytes(plateMaxBytes)}).`}
|
: `This document is approaching the rich editor limit (${formatBytes(activeMarkdownSizeBytes)} of ${formatBytes(plateMaxBytes)}, ${activeMarkdownLineCount.toLocaleString()} of ${plateMaxLines.toLocaleString()} lines).`}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<PlateEditor
|
<PlateErrorBoundary
|
||||||
key={`${
|
key={`plate-boundary-${editorInstanceKey}`}
|
||||||
isMemoryMode
|
fallback={
|
||||||
? `memory-${memoryScope ?? "user"}`
|
<SourceCodeEditor
|
||||||
: isLocalFileMode
|
path={`${editorDoc.title || "document"}.md`}
|
||||||
? (localFilePath ?? "local-file")
|
language="markdown"
|
||||||
: documentId
|
value={editorDoc.source_markdown}
|
||||||
}-${isEditing ? "editing" : "viewing"}`}
|
readOnly
|
||||||
preset="full"
|
onChange={() => {}}
|
||||||
markdown={editorDoc.source_markdown}
|
/>
|
||||||
onMarkdownChange={handleMarkdownChange}
|
}
|
||||||
readOnly={!isEditing}
|
>
|
||||||
placeholder="Start writing..."
|
<PlateEditor
|
||||||
editorVariant="default"
|
key={editorInstanceKey}
|
||||||
allowModeToggle={false}
|
preset="full"
|
||||||
reserveToolbarSpace
|
markdown={editorDoc.source_markdown}
|
||||||
defaultEditing={isEditing}
|
onMarkdownChange={handleMarkdownChange}
|
||||||
className="**:[[role=toolbar]]:bg-sidebar!"
|
readOnly={!isEditing}
|
||||||
// Render `[citation:N]` badges in view mode only.
|
placeholder="Start writing..."
|
||||||
// Edit mode keeps raw text so the user can edit/delete
|
editorVariant="default"
|
||||||
// tokens directly. `local_file` never reaches this branch
|
allowModeToggle={false}
|
||||||
// (handled by the source_code editor above).
|
reserveToolbarSpace
|
||||||
enableCitations={!isEditing && !isLocalFileMode && !isMemoryMode}
|
defaultEditing={isEditing}
|
||||||
/>
|
className="**:[[role=toolbar]]:bg-sidebar!"
|
||||||
|
// Render `[citation:N]` badges in view mode only.
|
||||||
|
// Edit mode keeps raw text so the user can edit/delete
|
||||||
|
// tokens directly. `local_file` never reaches this branch
|
||||||
|
// (handled by the source_code editor above).
|
||||||
|
enableCitations={!isEditing && !isLocalFileMode && !isMemoryMode}
|
||||||
|
/>
|
||||||
|
</PlateErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
34
surfsense_web/components/editor/plate-error-boundary.tsx
Normal file
34
surfsense_web/components/editor/plate-error-boundary.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Component, type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface PlateErrorBoundaryProps {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlateErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlateErrorBoundary extends Component<
|
||||||
|
PlateErrorBoundaryProps,
|
||||||
|
PlateErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: PlateErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): PlateErrorBoundaryState {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue