mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
feat: improve document editor panel behavior
This commit is contained in:
parent
89a8438864
commit
cb1cf26ef3
8 changed files with 380 additions and 81 deletions
|
|
@ -3,10 +3,11 @@ import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right
|
||||||
|
|
||||||
interface EditorPanelState {
|
interface EditorPanelState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
kind: "document" | "local_file";
|
kind: "document" | "local_file" | "memory";
|
||||||
documentId: number | null;
|
documentId: number | null;
|
||||||
localFilePath: string | null;
|
localFilePath: string | null;
|
||||||
searchSpaceId: number | null;
|
searchSpaceId: number | null;
|
||||||
|
memoryScope: "user" | "team" | null;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,6 +17,7 @@ const initialState: EditorPanelState = {
|
||||||
documentId: null,
|
documentId: null,
|
||||||
localFilePath: null,
|
localFilePath: null,
|
||||||
searchSpaceId: null,
|
searchSpaceId: null,
|
||||||
|
memoryScope: null,
|
||||||
title: null,
|
title: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -38,6 +40,12 @@ export const openEditorPanelAtom = atom(
|
||||||
title?: string;
|
title?: string;
|
||||||
searchSpaceId?: number;
|
searchSpaceId?: number;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
kind: "memory";
|
||||||
|
memoryScope: "user" | "team";
|
||||||
|
title?: string;
|
||||||
|
searchSpaceId?: number;
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
if (!get(editorPanelAtom).isOpen) {
|
if (!get(editorPanelAtom).isOpen) {
|
||||||
set(preEditorCollapsedAtom, get(rightPanelCollapsedAtom));
|
set(preEditorCollapsedAtom, get(rightPanelCollapsedAtom));
|
||||||
|
|
@ -49,6 +57,21 @@ export const openEditorPanelAtom = atom(
|
||||||
documentId: null,
|
documentId: null,
|
||||||
localFilePath: payload.localFilePath,
|
localFilePath: payload.localFilePath,
|
||||||
searchSpaceId: payload.searchSpaceId ?? null,
|
searchSpaceId: payload.searchSpaceId ?? null,
|
||||||
|
memoryScope: null,
|
||||||
|
title: payload.title ?? null,
|
||||||
|
});
|
||||||
|
set(rightPanelTabAtom, "editor");
|
||||||
|
set(rightPanelCollapsedAtom, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (payload.kind === "memory") {
|
||||||
|
set(editorPanelAtom, {
|
||||||
|
isOpen: true,
|
||||||
|
kind: "memory",
|
||||||
|
documentId: null,
|
||||||
|
localFilePath: null,
|
||||||
|
searchSpaceId: payload.searchSpaceId ?? null,
|
||||||
|
memoryScope: payload.memoryScope,
|
||||||
title: payload.title ?? null,
|
title: payload.title ?? null,
|
||||||
});
|
});
|
||||||
set(rightPanelTabAtom, "editor");
|
set(rightPanelTabAtom, "editor");
|
||||||
|
|
@ -61,6 +84,7 @@ export const openEditorPanelAtom = atom(
|
||||||
documentId: payload.documentId,
|
documentId: payload.documentId,
|
||||||
localFilePath: null,
|
localFilePath: null,
|
||||||
searchSpaceId: payload.searchSpaceId,
|
searchSpaceId: payload.searchSpaceId,
|
||||||
|
memoryScope: null,
|
||||||
title: payload.title ?? null,
|
title: payload.title ?? null,
|
||||||
});
|
});
|
||||||
set(rightPanelTabAtom, "editor");
|
set(rightPanelTabAtom, "editor");
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Move,
|
Move,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
RotateCcw,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
|
|
@ -61,8 +62,13 @@ interface DocumentNodeProps {
|
||||||
onEdit: (doc: DocumentNodeDoc) => void;
|
onEdit: (doc: DocumentNodeDoc) => void;
|
||||||
onDelete: (doc: DocumentNodeDoc) => void;
|
onDelete: (doc: DocumentNodeDoc) => void;
|
||||||
onMove: (doc: DocumentNodeDoc) => void;
|
onMove: (doc: DocumentNodeDoc) => void;
|
||||||
|
onReset?: (doc: DocumentNodeDoc) => void;
|
||||||
onExport?: (doc: DocumentNodeDoc, format: string) => void;
|
onExport?: (doc: DocumentNodeDoc, format: string) => void;
|
||||||
onVersionHistory?: (doc: DocumentNodeDoc) => void;
|
onVersionHistory?: (doc: DocumentNodeDoc) => void;
|
||||||
|
canDelete?: boolean;
|
||||||
|
canMove?: boolean;
|
||||||
|
canMention?: boolean;
|
||||||
|
canEdit?: boolean;
|
||||||
contextMenuOpen?: boolean;
|
contextMenuOpen?: boolean;
|
||||||
onContextMenuOpenChange?: (open: boolean) => void;
|
onContextMenuOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -76,8 +82,13 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onMove,
|
onMove,
|
||||||
|
onReset,
|
||||||
onExport,
|
onExport,
|
||||||
onVersionHistory,
|
onVersionHistory,
|
||||||
|
canDelete = true,
|
||||||
|
canMove = true,
|
||||||
|
canMention = true,
|
||||||
|
canEdit = true,
|
||||||
contextMenuOpen,
|
contextMenuOpen,
|
||||||
onContextMenuOpenChange,
|
onContextMenuOpenChange,
|
||||||
}: DocumentNodeProps) {
|
}: DocumentNodeProps) {
|
||||||
|
|
@ -85,8 +96,13 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
const isFailed = statusState === "failed";
|
const isFailed = statusState === "failed";
|
||||||
const isProcessing = statusState === "pending" || statusState === "processing";
|
const isProcessing = statusState === "pending" || statusState === "processing";
|
||||||
const isUnavailable = isProcessing || isFailed;
|
const isUnavailable = isProcessing || isFailed;
|
||||||
const isSelectable = !isUnavailable;
|
const isMemoryDocument =
|
||||||
const isEditable = EDITABLE_DOCUMENT_TYPES.has(doc.document_type) && !isUnavailable;
|
doc.document_type === "USER_MEMORY" || doc.document_type === "TEAM_MEMORY";
|
||||||
|
const isSelectable = canMention && !isUnavailable;
|
||||||
|
const isEditable =
|
||||||
|
canEdit &&
|
||||||
|
(isMemoryDocument || EDITABLE_DOCUMENT_TYPES.has(doc.document_type)) &&
|
||||||
|
!isUnavailable;
|
||||||
|
|
||||||
const handleCheckChange = useCallback(() => {
|
const handleCheckChange = useCallback(() => {
|
||||||
if (isSelectable) {
|
if (isSelectable) {
|
||||||
|
|
@ -94,13 +110,22 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
}
|
}
|
||||||
}, [doc, isMentioned, isSelectable, onToggleChatMention]);
|
}, [doc, isMentioned, isSelectable, onToggleChatMention]);
|
||||||
|
|
||||||
|
const handlePrimaryClick = useCallback(() => {
|
||||||
|
if (canMention) {
|
||||||
|
handleCheckChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onPreview(doc);
|
||||||
|
}, [canMention, doc, handleCheckChange, onPreview]);
|
||||||
|
|
||||||
const [{ isDragging }, drag] = useDrag(
|
const [{ isDragging }, drag] = useDrag(
|
||||||
() => ({
|
() => ({
|
||||||
type: DND_TYPES.DOCUMENT,
|
type: DND_TYPES.DOCUMENT,
|
||||||
item: { id: doc.id },
|
item: { id: doc.id },
|
||||||
|
canDrag: canMove,
|
||||||
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
||||||
}),
|
}),
|
||||||
[doc.id]
|
[canMove, doc.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
|
@ -130,9 +155,11 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
const attachRef = useCallback(
|
const attachRef = useCallback(
|
||||||
(node: HTMLDivElement | null) => {
|
(node: HTMLDivElement | null) => {
|
||||||
(rowRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
(rowRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
||||||
|
if (canMove) {
|
||||||
drag(node);
|
drag(node);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[drag]
|
[canMove, drag]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -187,12 +214,39 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{isMemoryDocument ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-disabled="true"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="h-3.5 w-3.5 shrink-0 cursor-default"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={false}
|
||||||
|
disabled
|
||||||
|
aria-disabled
|
||||||
|
className="h-3.5 w-3.5 pointer-events-none"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : canMention ? (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isMentioned}
|
checked={isMentioned}
|
||||||
onCheckedChange={handleCheckChange}
|
onCheckedChange={handleCheckChange}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="h-3.5 w-3.5 shrink-0"
|
className="h-3.5 w-3.5 shrink-0"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||||
|
{getDocumentTypeIcon(
|
||||||
|
doc.document_type as DocumentTypeEnum,
|
||||||
|
"h-3.5 w-3.5 text-muted-foreground"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
|
@ -205,8 +259,8 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-disabled={!isSelectable}
|
aria-disabled={canMention ? !isSelectable : false}
|
||||||
onClick={handleCheckChange}
|
onClick={handlePrimaryClick}
|
||||||
className="h-full min-w-0 flex-1 justify-start bg-transparent px-0 py-0 text-left font-normal text-inherit hover:bg-transparent hover:text-inherit"
|
className="h-full min-w-0 flex-1 justify-start bg-transparent px-0 py-0 text-left font-normal text-inherit hover:bg-transparent hover:text-inherit"
|
||||||
>
|
>
|
||||||
<span ref={titleRef} className="min-w-0 flex-1 truncate">
|
<span ref={titleRef} className="min-w-0 flex-1 truncate">
|
||||||
|
|
@ -268,10 +322,12 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{canMove && (
|
||||||
<DropdownMenuItem onClick={() => onMove(doc)}>
|
<DropdownMenuItem onClick={() => onMove(doc)}>
|
||||||
<Move className="mr-2 h-4 w-4" />
|
<Move className="mr-2 h-4 w-4" />
|
||||||
Move to...
|
Move to...
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
{onExport && (
|
{onExport && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger disabled={isUnavailable}>
|
<DropdownMenuSubTrigger disabled={isUnavailable}>
|
||||||
|
|
@ -289,10 +345,18 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
Versions
|
Versions
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{isMemoryDocument && onReset && (
|
||||||
|
<DropdownMenuItem onClick={() => onReset(doc)}>
|
||||||
|
<RotateCcw className="mr-2 h-4 w-4" />
|
||||||
|
Reset
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
<DropdownMenuItem disabled={isProcessing} onClick={() => onDelete(doc)}>
|
<DropdownMenuItem disabled={isProcessing} onClick={() => onDelete(doc)}>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -311,10 +375,12 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
Edit
|
Edit
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{canMove && (
|
||||||
<ContextMenuItem onClick={() => onMove(doc)}>
|
<ContextMenuItem onClick={() => onMove(doc)}>
|
||||||
<Move className="mr-2 h-4 w-4" />
|
<Move className="mr-2 h-4 w-4" />
|
||||||
Move to...
|
Move to...
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
{onExport && (
|
{onExport && (
|
||||||
<ContextMenuSub>
|
<ContextMenuSub>
|
||||||
<ContextMenuSubTrigger disabled={isUnavailable}>
|
<ContextMenuSubTrigger disabled={isUnavailable}>
|
||||||
|
|
@ -332,10 +398,18 @@ export const DocumentNode = React.memo(function DocumentNode({
|
||||||
Versions
|
Versions
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{isMemoryDocument && onReset && (
|
||||||
|
<ContextMenuItem onClick={() => onReset(doc)}>
|
||||||
|
<RotateCcw className="mr-2 h-4 w-4" />
|
||||||
|
Reset
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
<ContextMenuItem disabled={isProcessing} onClick={() => onDelete(doc)}>
|
<ContextMenuItem disabled={isProcessing} onClick={() => onDelete(doc)}>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete
|
Delete
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ interface FolderTreeViewProps {
|
||||||
onEditDocument: (doc: DocumentNodeDoc) => void;
|
onEditDocument: (doc: DocumentNodeDoc) => void;
|
||||||
onDeleteDocument: (doc: DocumentNodeDoc) => void;
|
onDeleteDocument: (doc: DocumentNodeDoc) => void;
|
||||||
onMoveDocument: (doc: DocumentNodeDoc) => void;
|
onMoveDocument: (doc: DocumentNodeDoc) => void;
|
||||||
|
onResetDocument?: (doc: DocumentNodeDoc) => void;
|
||||||
onExportDocument?: (doc: DocumentNodeDoc, format: string) => void;
|
onExportDocument?: (doc: DocumentNodeDoc, format: string) => void;
|
||||||
onVersionHistory?: (doc: DocumentNodeDoc) => void;
|
onVersionHistory?: (doc: DocumentNodeDoc) => void;
|
||||||
activeTypes: DocumentTypeEnum[];
|
activeTypes: DocumentTypeEnum[];
|
||||||
|
|
@ -74,6 +75,7 @@ export function FolderTreeView({
|
||||||
onEditDocument,
|
onEditDocument,
|
||||||
onDeleteDocument,
|
onDeleteDocument,
|
||||||
onMoveDocument,
|
onMoveDocument,
|
||||||
|
onResetDocument,
|
||||||
onExportDocument,
|
onExportDocument,
|
||||||
onVersionHistory,
|
onVersionHistory,
|
||||||
activeTypes,
|
activeTypes,
|
||||||
|
|
@ -236,6 +238,47 @@ export function FolderTreeView({
|
||||||
return states;
|
return states;
|
||||||
}, [folders, docsByFolder, foldersByParent, folderMap]);
|
}, [folders, docsByFolder, foldersByParent, folderMap]);
|
||||||
|
|
||||||
|
const renderDocumentNode = useCallback(
|
||||||
|
(d: DocumentNodeDoc, depth: number) => {
|
||||||
|
const isMemoryDocument =
|
||||||
|
d.document_type === "USER_MEMORY" || d.document_type === "TEAM_MEMORY";
|
||||||
|
return (
|
||||||
|
<DocumentNode
|
||||||
|
key={`doc-${d.id}`}
|
||||||
|
doc={d}
|
||||||
|
depth={depth}
|
||||||
|
isMentioned={!isMemoryDocument && mentionedDocKeys.has(getMentionDocKey(d))}
|
||||||
|
onToggleChatMention={onToggleChatMention}
|
||||||
|
onPreview={onPreviewDocument}
|
||||||
|
onEdit={onEditDocument}
|
||||||
|
onDelete={onDeleteDocument}
|
||||||
|
onMove={onMoveDocument}
|
||||||
|
onReset={onResetDocument}
|
||||||
|
onExport={isMemoryDocument ? undefined : onExportDocument}
|
||||||
|
onVersionHistory={isMemoryDocument ? undefined : onVersionHistory}
|
||||||
|
canDelete={!isMemoryDocument}
|
||||||
|
canMove={!isMemoryDocument}
|
||||||
|
canMention={!isMemoryDocument}
|
||||||
|
canEdit
|
||||||
|
contextMenuOpen={openContextMenuId === `doc-${d.id}`}
|
||||||
|
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `doc-${d.id}` : null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
mentionedDocKeys,
|
||||||
|
onDeleteDocument,
|
||||||
|
onEditDocument,
|
||||||
|
onExportDocument,
|
||||||
|
onMoveDocument,
|
||||||
|
onPreviewDocument,
|
||||||
|
onResetDocument,
|
||||||
|
onToggleChatMention,
|
||||||
|
onVersionHistory,
|
||||||
|
openContextMenuId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
function renderLevel(parentId: number | null, depth: number): React.ReactNode[] {
|
function renderLevel(parentId: number | null, depth: number): React.ReactNode[] {
|
||||||
const key = parentId ?? "root";
|
const key = parentId ?? "root";
|
||||||
const childFolders = (foldersByParent[key] ?? []).slice().sort((a, b) => {
|
const childFolders = (foldersByParent[key] ?? []).slice().sort((a, b) => {
|
||||||
|
|
@ -263,23 +306,7 @@ export function FolderTreeView({
|
||||||
return state === "pending" || state === "processing";
|
return state === "pending" || state === "processing";
|
||||||
});
|
});
|
||||||
for (const d of processingDocs) {
|
for (const d of processingDocs) {
|
||||||
nodes.push(
|
nodes.push(renderDocumentNode(d, depth));
|
||||||
<DocumentNode
|
|
||||||
key={`doc-${d.id}`}
|
|
||||||
doc={d}
|
|
||||||
depth={depth}
|
|
||||||
isMentioned={mentionedDocKeys.has(getMentionDocKey(d))}
|
|
||||||
onToggleChatMention={onToggleChatMention}
|
|
||||||
onPreview={onPreviewDocument}
|
|
||||||
onEdit={onEditDocument}
|
|
||||||
onDelete={onDeleteDocument}
|
|
||||||
onMove={onMoveDocument}
|
|
||||||
onExport={onExportDocument}
|
|
||||||
onVersionHistory={onVersionHistory}
|
|
||||||
contextMenuOpen={openContextMenuId === `doc-${d.id}`}
|
|
||||||
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `doc-${d.id}` : null)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,23 +370,7 @@ export function FolderTreeView({
|
||||||
: childDocs;
|
: childDocs;
|
||||||
|
|
||||||
for (const d of remainingDocs) {
|
for (const d of remainingDocs) {
|
||||||
nodes.push(
|
nodes.push(renderDocumentNode(d, depth));
|
||||||
<DocumentNode
|
|
||||||
key={`doc-${d.id}`}
|
|
||||||
doc={d}
|
|
||||||
depth={depth}
|
|
||||||
isMentioned={mentionedDocKeys.has(getMentionDocKey(d))}
|
|
||||||
onToggleChatMention={onToggleChatMention}
|
|
||||||
onPreview={onPreviewDocument}
|
|
||||||
onEdit={onEditDocument}
|
|
||||||
onDelete={onDeleteDocument}
|
|
||||||
onMove={onMoveDocument}
|
|
||||||
onExport={onExportDocument}
|
|
||||||
onVersionHistory={onVersionHistory}
|
|
||||||
contextMenuOpen={openContextMenuId === `doc-${d.id}`}
|
|
||||||
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `doc-${d.id}` : null)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes;
|
return nodes;
|
||||||
|
|
|
||||||
|
|
@ -107,13 +107,15 @@ export function EditorPanelContent({
|
||||||
kind = "document",
|
kind = "document",
|
||||||
documentId,
|
documentId,
|
||||||
localFilePath,
|
localFilePath,
|
||||||
|
memoryScope,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
title,
|
title,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
kind?: "document" | "local_file";
|
kind?: "document" | "local_file" | "memory";
|
||||||
documentId?: number;
|
documentId?: number;
|
||||||
localFilePath?: string;
|
localFilePath?: string;
|
||||||
|
memoryScope?: "user" | "team";
|
||||||
searchSpaceId?: number;
|
searchSpaceId?: number;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
|
@ -135,6 +137,7 @@ export function EditorPanelContent({
|
||||||
const changeCountRef = useRef(0);
|
const changeCountRef = useRef(0);
|
||||||
const [displayTitle, setDisplayTitle] = useState(title || "Untitled");
|
const [displayTitle, setDisplayTitle] = useState(title || "Untitled");
|
||||||
const isLocalFileMode = kind === "local_file";
|
const isLocalFileMode = kind === "local_file";
|
||||||
|
const isMemoryMode = kind === "memory";
|
||||||
const editorRenderMode: EditorRenderMode = isLocalFileMode ? "source_code" : "rich_markdown";
|
const editorRenderMode: EditorRenderMode = isLocalFileMode ? "source_code" : "rich_markdown";
|
||||||
|
|
||||||
const resolveLocalVirtualPath = useCallback(
|
const resolveLocalVirtualPath = useCallback(
|
||||||
|
|
@ -199,6 +202,39 @@ export function EditorPanelContent({
|
||||||
initialLoadDone.current = true;
|
initialLoadDone.current = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isMemoryMode) {
|
||||||
|
if (memoryScope === "team" && !searchSpaceId) {
|
||||||
|
throw new Error("Missing search space context");
|
||||||
|
}
|
||||||
|
const response = await authenticatedFetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${
|
||||||
|
memoryScope === "team"
|
||||||
|
? `/api/v1/searchspaces/${searchSpaceId}/memory`
|
||||||
|
: "/api/v1/users/me/memory"
|
||||||
|
}`,
|
||||||
|
{ method: "GET" }
|
||||||
|
);
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ detail: "Failed to fetch memory" }));
|
||||||
|
throw new Error(errorData.detail || "Failed to fetch memory");
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as { memory_md?: string };
|
||||||
|
const content: EditorContent = {
|
||||||
|
document_id: memoryScope === "team" ? -1002 : -1001,
|
||||||
|
title: title || (memoryScope === "team" ? "Team Memory" : "Personal Memory"),
|
||||||
|
document_type: memoryScope === "team" ? "TEAM_MEMORY" : "USER_MEMORY",
|
||||||
|
source_markdown: data.memory_md ?? "",
|
||||||
|
};
|
||||||
|
markdownRef.current = content.source_markdown;
|
||||||
|
setDisplayTitle(content.title);
|
||||||
|
setEditorDoc(content);
|
||||||
|
initialLoadDone.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!documentId || !searchSpaceId) {
|
if (!documentId || !searchSpaceId) {
|
||||||
throw new Error("Missing document context");
|
throw new Error("Missing document context");
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +289,9 @@ export function EditorPanelContent({
|
||||||
documentId,
|
documentId,
|
||||||
electronAPI,
|
electronAPI,
|
||||||
isLocalFileMode,
|
isLocalFileMode,
|
||||||
|
isMemoryMode,
|
||||||
localFilePath,
|
localFilePath,
|
||||||
|
memoryScope,
|
||||||
resolveLocalVirtualPath,
|
resolveLocalVirtualPath,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
title,
|
title,
|
||||||
|
|
@ -316,6 +354,39 @@ export function EditorPanelContent({
|
||||||
setEditedMarkdown(markdownRef.current === contentToSave ? null : markdownRef.current);
|
setEditedMarkdown(markdownRef.current === contentToSave ? null : markdownRef.current);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (isMemoryMode) {
|
||||||
|
if (memoryScope === "team" && !searchSpaceId) {
|
||||||
|
throw new Error("Missing search space context");
|
||||||
|
}
|
||||||
|
const response = await authenticatedFetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${
|
||||||
|
memoryScope === "team"
|
||||||
|
? `/api/v1/searchspaces/${searchSpaceId}/memory`
|
||||||
|
: "/api/v1/users/me/memory"
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ memory_md: markdownRef.current }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ detail: "Failed to save memory" }));
|
||||||
|
throw new Error(errorData.detail || "Failed to save memory");
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as { memory_md?: string };
|
||||||
|
const savedContent = data.memory_md ?? markdownRef.current;
|
||||||
|
markdownRef.current = savedContent;
|
||||||
|
setEditorDoc((prev) => (prev ? { ...prev, source_markdown: savedContent } : prev));
|
||||||
|
setEditedMarkdown(null);
|
||||||
|
if (!options?.silent) {
|
||||||
|
toast.success("Memory saved");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!searchSpaceId || !documentId) {
|
if (!searchSpaceId || !documentId) {
|
||||||
throw new Error("Missing document context");
|
throw new Error("Missing document context");
|
||||||
}
|
}
|
||||||
|
|
@ -361,14 +432,17 @@ export function EditorPanelContent({
|
||||||
documentId,
|
documentId,
|
||||||
electronAPI,
|
electronAPI,
|
||||||
isLocalFileMode,
|
isLocalFileMode,
|
||||||
|
isMemoryMode,
|
||||||
localFilePath,
|
localFilePath,
|
||||||
|
memoryScope,
|
||||||
resolveLocalVirtualPath,
|
resolveLocalVirtualPath,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEditableType = editorDoc
|
const isEditableType = editorDoc
|
||||||
? (editorRenderMode === "source_code" ||
|
? (isMemoryMode ||
|
||||||
|
editorRenderMode === "source_code" ||
|
||||||
EDITABLE_DOCUMENT_TYPES.has(editorDoc.document_type ?? "")) &&
|
EDITABLE_DOCUMENT_TYPES.has(editorDoc.document_type ?? "")) &&
|
||||||
!isLargeDocument
|
!isLargeDocument
|
||||||
: false;
|
: false;
|
||||||
|
|
@ -495,7 +569,7 @@ export function EditorPanelContent({
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!isLocalFileMode && editorDoc?.document_type && documentId && (
|
{!isLocalFileMode && !isMemoryMode && editorDoc?.document_type && documentId && (
|
||||||
<VersionHistoryButton
|
<VersionHistoryButton
|
||||||
documentId={documentId}
|
documentId={documentId}
|
||||||
documentType={editorDoc.document_type}
|
documentType={editorDoc.document_type}
|
||||||
|
|
@ -568,7 +642,7 @@ export function EditorPanelContent({
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!isLocalFileMode && editorDoc?.document_type && documentId && (
|
{!isLocalFileMode && !isMemoryMode && editorDoc?.document_type && documentId && (
|
||||||
<VersionHistoryButton
|
<VersionHistoryButton
|
||||||
documentId={documentId}
|
documentId={documentId}
|
||||||
documentType={editorDoc.document_type}
|
documentType={editorDoc.document_type}
|
||||||
|
|
@ -664,7 +738,13 @@ export function EditorPanelContent({
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<PlateEditor
|
<PlateEditor
|
||||||
key={`${isLocalFileMode ? (localFilePath ?? "local-file") : documentId}-${isEditing ? "editing" : "viewing"}`}
|
key={`${
|
||||||
|
isMemoryMode
|
||||||
|
? `memory-${memoryScope ?? "user"}`
|
||||||
|
: isLocalFileMode
|
||||||
|
? (localFilePath ?? "local-file")
|
||||||
|
: documentId
|
||||||
|
}-${isEditing ? "editing" : "viewing"}`}
|
||||||
preset="full"
|
preset="full"
|
||||||
markdown={editorDoc.source_markdown}
|
markdown={editorDoc.source_markdown}
|
||||||
onMarkdownChange={handleMarkdownChange}
|
onMarkdownChange={handleMarkdownChange}
|
||||||
|
|
@ -679,7 +759,7 @@ export function EditorPanelContent({
|
||||||
// Edit mode keeps raw text so the user can edit/delete
|
// Edit mode keeps raw text so the user can edit/delete
|
||||||
// tokens directly. `local_file` never reaches this branch
|
// tokens directly. `local_file` never reaches this branch
|
||||||
// (handled by the source_code editor above).
|
// (handled by the source_code editor above).
|
||||||
enableCitations={!isEditing && !isLocalFileMode}
|
enableCitations={!isEditing && !isLocalFileMode && !isMemoryMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -708,7 +788,9 @@ function DesktopEditorPanel() {
|
||||||
const hasTarget =
|
const hasTarget =
|
||||||
panelState.kind === "document"
|
panelState.kind === "document"
|
||||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||||
: !!panelState.localFilePath;
|
: panelState.kind === "local_file"
|
||||||
|
? !!panelState.localFilePath
|
||||||
|
: !!panelState.memoryScope;
|
||||||
if (!panelState.isOpen || !hasTarget) return null;
|
if (!panelState.isOpen || !hasTarget) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -717,6 +799,7 @@ function DesktopEditorPanel() {
|
||||||
kind={panelState.kind}
|
kind={panelState.kind}
|
||||||
documentId={panelState.documentId ?? undefined}
|
documentId={panelState.documentId ?? undefined}
|
||||||
localFilePath={panelState.localFilePath ?? undefined}
|
localFilePath={panelState.localFilePath ?? undefined}
|
||||||
|
memoryScope={panelState.memoryScope ?? undefined}
|
||||||
searchSpaceId={panelState.searchSpaceId ?? undefined}
|
searchSpaceId={panelState.searchSpaceId ?? undefined}
|
||||||
title={panelState.title}
|
title={panelState.title}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
|
|
@ -734,7 +817,7 @@ function MobileEditorDrawer() {
|
||||||
const hasTarget =
|
const hasTarget =
|
||||||
panelState.kind === "document"
|
panelState.kind === "document"
|
||||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||||
: !!panelState.localFilePath;
|
: !!panelState.memoryScope;
|
||||||
if (!hasTarget) return null;
|
if (!hasTarget) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -756,6 +839,7 @@ function MobileEditorDrawer() {
|
||||||
kind={panelState.kind}
|
kind={panelState.kind}
|
||||||
documentId={panelState.documentId ?? undefined}
|
documentId={panelState.documentId ?? undefined}
|
||||||
localFilePath={panelState.localFilePath ?? undefined}
|
localFilePath={panelState.localFilePath ?? undefined}
|
||||||
|
memoryScope={panelState.memoryScope ?? undefined}
|
||||||
searchSpaceId={panelState.searchSpaceId ?? undefined}
|
searchSpaceId={panelState.searchSpaceId ?? undefined}
|
||||||
title={panelState.title}
|
title={panelState.title}
|
||||||
/>
|
/>
|
||||||
|
|
@ -771,7 +855,9 @@ export function EditorPanel() {
|
||||||
const hasTarget =
|
const hasTarget =
|
||||||
panelState.kind === "document"
|
panelState.kind === "document"
|
||||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||||
: !!panelState.localFilePath;
|
: panelState.kind === "local_file"
|
||||||
|
? !!panelState.localFilePath
|
||||||
|
: !!panelState.memoryScope;
|
||||||
|
|
||||||
if (!panelState.isOpen || !hasTarget) return null;
|
if (!panelState.isOpen || !hasTarget) return null;
|
||||||
if (!isDesktop && panelState.kind === "local_file") return null;
|
if (!isDesktop && panelState.kind === "local_file") return null;
|
||||||
|
|
@ -789,7 +875,9 @@ export function MobileEditorPanel() {
|
||||||
const hasTarget =
|
const hasTarget =
|
||||||
panelState.kind === "document"
|
panelState.kind === "document"
|
||||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||||
: !!panelState.localFilePath;
|
: panelState.kind === "local_file"
|
||||||
|
? !!panelState.localFilePath
|
||||||
|
: !!panelState.memoryScope;
|
||||||
|
|
||||||
if (isDesktop || !panelState.isOpen || !hasTarget || panelState.kind === "local_file")
|
if (isDesktop || !panelState.isOpen || !hasTarget || panelState.kind === "local_file")
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,11 @@ export function RightPanelToggleButton({
|
||||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||||
const editorOpen =
|
const editorOpen =
|
||||||
editorState.isOpen &&
|
editorState.isOpen &&
|
||||||
(editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
|
(editorState.kind === "document"
|
||||||
|
? !!editorState.documentId
|
||||||
|
: editorState.kind === "memory"
|
||||||
|
? !!editorState.memoryScope
|
||||||
|
: !!editorState.localFilePath);
|
||||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||||
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
||||||
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
|
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
|
||||||
|
|
@ -151,7 +155,11 @@ export function RightPanelExpandButton() {
|
||||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||||
const editorOpen =
|
const editorOpen =
|
||||||
editorState.isOpen &&
|
editorState.isOpen &&
|
||||||
(editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
|
(editorState.kind === "document"
|
||||||
|
? !!editorState.documentId
|
||||||
|
: editorState.kind === "memory"
|
||||||
|
? !!editorState.memoryScope
|
||||||
|
: !!editorState.localFilePath);
|
||||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||||
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
||||||
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
|
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
|
||||||
|
|
@ -193,7 +201,11 @@ export function RightPanel({
|
||||||
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
const reportOpen = reportState.isOpen && !!reportState.reportId;
|
||||||
const editorOpen =
|
const editorOpen =
|
||||||
editorState.isOpen &&
|
editorState.isOpen &&
|
||||||
(editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
|
(editorState.kind === "document"
|
||||||
|
? !!editorState.documentId
|
||||||
|
: editorState.kind === "memory"
|
||||||
|
? !!editorState.memoryScope
|
||||||
|
: !!editorState.localFilePath);
|
||||||
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
|
||||||
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
const citationOpen = citationState.isOpen && citationState.chunkId != null;
|
||||||
|
|
||||||
|
|
@ -292,6 +304,7 @@ export function RightPanel({
|
||||||
kind={editorState.kind}
|
kind={editorState.kind}
|
||||||
documentId={editorState.documentId ?? undefined}
|
documentId={editorState.documentId ?? undefined}
|
||||||
localFilePath={editorState.localFilePath ?? undefined}
|
localFilePath={editorState.localFilePath ?? undefined}
|
||||||
|
memoryScope={editorState.memoryScope ?? undefined}
|
||||||
searchSpaceId={editorState.searchSpaceId ?? undefined}
|
searchSpaceId={editorState.searchSpaceId ?? undefined}
|
||||||
title={editorState.title}
|
title={editorState.title}
|
||||||
onClose={closeEditor}
|
onClose={closeEditor}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,31 @@ const DesktopLocalTabContent = dynamic(
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const NON_DELETABLE_DOCUMENT_TYPES: readonly string[] = ["SURFSENSE_DOCS"];
|
const NON_DELETABLE_DOCUMENT_TYPES: readonly string[] = [
|
||||||
|
"SURFSENSE_DOCS",
|
||||||
|
"USER_MEMORY",
|
||||||
|
"TEAM_MEMORY",
|
||||||
|
];
|
||||||
|
const MEMORY_DOCUMENTS: DocumentNodeDoc[] = [
|
||||||
|
{
|
||||||
|
id: -1001,
|
||||||
|
title: "MEMORY.md",
|
||||||
|
document_type: "USER_MEMORY",
|
||||||
|
folderId: null,
|
||||||
|
status: { state: "ready" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: -1002,
|
||||||
|
title: "TEAM_MEMORY.md",
|
||||||
|
document_type: "TEAM_MEMORY",
|
||||||
|
folderId: null,
|
||||||
|
status: { state: "ready" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function isMemoryDocument(doc: { document_type: string }) {
|
||||||
|
return doc.document_type === "USER_MEMORY" || doc.document_type === "TEAM_MEMORY";
|
||||||
|
}
|
||||||
const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
|
const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
|
||||||
const MAX_LOCAL_FILESYSTEM_ROOTS = 10;
|
const MAX_LOCAL_FILESYSTEM_ROOTS = 10;
|
||||||
|
|
||||||
|
|
@ -879,6 +903,7 @@ function AuthenticatedDocumentsSidebarBase({
|
||||||
|
|
||||||
const handleToggleChatMention = useCallback(
|
const handleToggleChatMention = useCallback(
|
||||||
(doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => {
|
(doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => {
|
||||||
|
if (isMemoryDocument(doc)) return;
|
||||||
const key = getMentionDocKey({ ...doc, kind: "doc" });
|
const key = getMentionDocKey({ ...doc, kind: "doc" });
|
||||||
if (isMentioned) {
|
if (isMentioned) {
|
||||||
setSidebarDocs((prev) => prev.filter((d) => getMentionDocKey(d) !== key));
|
setSidebarDocs((prev) => prev.filter((d) => getMentionDocKey(d) !== key));
|
||||||
|
|
@ -927,11 +952,66 @@ function AuthenticatedDocumentsSidebarBase({
|
||||||
[treeFolders, setSidebarDocs]
|
[treeFolders, setSidebarDocs]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const treeDocumentsWithMemory = useMemo(
|
||||||
|
() => [...MEMORY_DOCUMENTS, ...treeDocuments],
|
||||||
|
[treeDocuments]
|
||||||
|
);
|
||||||
|
|
||||||
const searchFilteredDocuments = useMemo(() => {
|
const searchFilteredDocuments = useMemo(() => {
|
||||||
const query = debouncedSearch.trim().toLowerCase();
|
const query = debouncedSearch.trim().toLowerCase();
|
||||||
if (!query) return treeDocuments;
|
if (!query) return treeDocumentsWithMemory;
|
||||||
return treeDocuments.filter((d) => d.title.toLowerCase().includes(query));
|
return treeDocumentsWithMemory.filter((d) => d.title.toLowerCase().includes(query));
|
||||||
}, [treeDocuments, debouncedSearch]);
|
}, [treeDocumentsWithMemory, debouncedSearch]);
|
||||||
|
|
||||||
|
const openMemoryDocument = useCallback(
|
||||||
|
(doc: DocumentNodeDoc) => {
|
||||||
|
if (doc.document_type === "USER_MEMORY") {
|
||||||
|
openEditorPanel({
|
||||||
|
kind: "memory",
|
||||||
|
memoryScope: "user",
|
||||||
|
searchSpaceId,
|
||||||
|
title: doc.title,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (doc.document_type === "TEAM_MEMORY") {
|
||||||
|
openEditorPanel({
|
||||||
|
kind: "memory",
|
||||||
|
memoryScope: "team",
|
||||||
|
searchSpaceId,
|
||||||
|
title: doc.title,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[openEditorPanel, searchSpaceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleResetMemoryDocument = useCallback(
|
||||||
|
async (doc: DocumentNodeDoc) => {
|
||||||
|
if (!isMemoryDocument(doc)) return;
|
||||||
|
if (!window.confirm(`Reset ${doc.title.toLowerCase()}? This clears the memory document.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const endpoint =
|
||||||
|
doc.document_type === "USER_MEMORY"
|
||||||
|
? `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/users/me/memory/reset`
|
||||||
|
: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory/reset`;
|
||||||
|
try {
|
||||||
|
const response = await authenticatedFetch(endpoint, { method: "POST" });
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ detail: "Reset failed" }));
|
||||||
|
throw new Error(errorData.detail || "Reset failed");
|
||||||
|
}
|
||||||
|
toast.success(`${doc.title} reset`);
|
||||||
|
openMemoryDocument(doc);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error((error as Error)?.message || `Failed to reset ${doc.title.toLowerCase()}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[openMemoryDocument, searchSpaceId]
|
||||||
|
);
|
||||||
|
|
||||||
const typeCounts = useMemo(() => {
|
const typeCounts = useMemo(() => {
|
||||||
const counts: Partial<Record<string, number>> = {};
|
const counts: Partial<Record<string, number>> = {};
|
||||||
|
|
@ -1169,6 +1249,7 @@ function AuthenticatedDocumentsSidebarBase({
|
||||||
onCreateFolder={handleCreateFolder}
|
onCreateFolder={handleCreateFolder}
|
||||||
searchQuery={debouncedSearch.trim() || undefined}
|
searchQuery={debouncedSearch.trim() || undefined}
|
||||||
onPreviewDocument={(doc) => {
|
onPreviewDocument={(doc) => {
|
||||||
|
if (openMemoryDocument(doc)) return;
|
||||||
openEditorPanel({
|
openEditorPanel({
|
||||||
documentId: doc.id,
|
documentId: doc.id,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
|
|
@ -1176,6 +1257,7 @@ function AuthenticatedDocumentsSidebarBase({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onEditDocument={(doc) => {
|
onEditDocument={(doc) => {
|
||||||
|
if (openMemoryDocument(doc)) return;
|
||||||
openEditorPanel({
|
openEditorPanel({
|
||||||
documentId: doc.id,
|
documentId: doc.id,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
|
|
@ -1184,6 +1266,7 @@ function AuthenticatedDocumentsSidebarBase({
|
||||||
}}
|
}}
|
||||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||||
onMoveDocument={handleMoveDocument}
|
onMoveDocument={handleMoveDocument}
|
||||||
|
onResetDocument={handleResetMemoryDocument}
|
||||||
onExportDocument={handleExportDocument}
|
onExportDocument={handleExportDocument}
|
||||||
onVersionHistory={(doc) => setVersionDocId(doc.id)}
|
onVersionHistory={(doc) => setVersionDocId(doc.id)}
|
||||||
activeTypes={activeTypes}
|
activeTypes={activeTypes}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { IconUsersGroup } from "@tabler/icons-react";
|
import { IconUsersGroup } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
Brain,
|
||||||
File,
|
File,
|
||||||
FileText,
|
FileText,
|
||||||
Globe,
|
Globe,
|
||||||
|
|
@ -120,6 +121,9 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
|
||||||
return <Webhook {...iconProps} />;
|
return <Webhook {...iconProps} />;
|
||||||
case "SURFSENSE_DOCS":
|
case "SURFSENSE_DOCS":
|
||||||
return <BookOpen {...iconProps} />;
|
return <BookOpen {...iconProps} />;
|
||||||
|
case "USER_MEMORY":
|
||||||
|
case "TEAM_MEMORY":
|
||||||
|
return <Brain {...iconProps} />;
|
||||||
case "DEEP":
|
case "DEEP":
|
||||||
return <Sparkles {...iconProps} />;
|
return <Sparkles {...iconProps} />;
|
||||||
case "DEEPER":
|
case "DEEPER":
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ export const documentTypeEnum = z.enum([
|
||||||
"LOCAL_FOLDER_FILE",
|
"LOCAL_FOLDER_FILE",
|
||||||
"SURFSENSE_DOCS",
|
"SURFSENSE_DOCS",
|
||||||
"NOTE",
|
"NOTE",
|
||||||
|
"USER_MEMORY",
|
||||||
|
"TEAM_MEMORY",
|
||||||
"COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
|
"COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
|
||||||
"COMPOSIO_GMAIL_CONNECTOR",
|
"COMPOSIO_GMAIL_CONNECTOR",
|
||||||
"COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
|
"COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue