Merge remote-tracking branch 'upstream/dev' into feat/opentelemetry

This commit is contained in:
Anish Sarkar 2026-05-21 00:46:40 +05:30
commit 7a0de6d377
79 changed files with 1992 additions and 2296 deletions

View file

@ -103,7 +103,11 @@ export function RightPanelToggleButton({
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen =
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 citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
@ -151,7 +155,11 @@ export function RightPanelExpandButton() {
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen =
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 citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
@ -193,7 +201,11 @@ export function RightPanel({
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen =
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 citationOpen = citationState.isOpen && citationState.chunkId != null;
@ -292,6 +304,7 @@ export function RightPanel({
kind={editorState.kind}
documentId={editorState.documentId ?? undefined}
localFilePath={editorState.localFilePath ?? undefined}
memoryScope={editorState.memoryScope ?? undefined}
searchSpaceId={editorState.searchSpaceId ?? undefined}
title={editorState.title}
onClose={closeEditor}

View file

@ -82,13 +82,50 @@ import { uploadFolderScan } from "@/lib/folder-sync-upload";
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
import { queries } from "@/zero/queries/index";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
import { BACKEND_URL } from "@/lib/env-config";
const DesktopLocalTabContent = dynamic(
() => import("./DesktopLocalTabContent").then((mod) => mod.DesktopLocalTabContent),
{ 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";
}
function downloadTextFile(content: string, fileName: string, type = "text/markdown;charset=utf-8") {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1";
const MAX_LOCAL_FILESYSTEM_ROOTS = 10;
@ -716,7 +753,7 @@ function AuthenticatedDocumentsSidebarBase({
.trim()
.slice(0, 80) || "folder";
await doExport(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${ctx.folder.id}`,
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${ctx.folder.id}`,
`${safeName}.zip`
);
toast.success(`Folder "${ctx.folder.name}" exported`);
@ -768,7 +805,7 @@ function AuthenticatedDocumentsSidebarBase({
.trim()
.slice(0, 80) || "folder";
await doExport(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${folder.id}`,
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${folder.id}`,
`${safeName}.zip`
);
toast.success(`Folder "${folder.name}" exported`);
@ -784,6 +821,30 @@ function AuthenticatedDocumentsSidebarBase({
const handleExportDocument = useCallback(
async (doc: DocumentNodeDoc, format: string) => {
if (isMemoryDocument(doc)) {
try {
const endpoint =
doc.document_type === "USER_MEMORY"
? `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/users/me/memory`
: `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory`;
const response = await authenticatedFetch(endpoint, { method: "GET" });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: "Export failed" }));
throw new Error(errorData.detail || "Export failed");
}
const data = (await response.json()) as { memory_md?: string };
downloadTextFile(
data.memory_md ?? "",
doc.title.endsWith(".md") ? doc.title : `${doc.title}.md`
);
return;
} catch (err) {
console.error("Memory export failed:", err);
toast.error(err instanceof Error ? err.message : "Export failed");
return;
}
}
const safeTitle =
doc.title
.replace(/[^a-zA-Z0-9 _-]/g, "_")
@ -793,7 +854,7 @@ function AuthenticatedDocumentsSidebarBase({
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${doc.id}/export?format=${format}`,
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${doc.id}/export?format=${format}`,
{ method: "GET" }
);
@ -879,6 +940,7 @@ function AuthenticatedDocumentsSidebarBase({
const handleToggleChatMention = useCallback(
(doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => {
if (isMemoryDocument(doc)) return;
const key = getMentionDocKey({ ...doc, kind: "doc" });
if (isMentioned) {
setSidebarDocs((prev) => prev.filter((d) => getMentionDocKey(d) !== key));
@ -927,11 +989,66 @@ function AuthenticatedDocumentsSidebarBase({
[treeFolders, setSidebarDocs]
);
const treeDocumentsWithMemory = useMemo(
() => [...MEMORY_DOCUMENTS, ...treeDocuments],
[treeDocuments]
);
const searchFilteredDocuments = useMemo(() => {
const query = debouncedSearch.trim().toLowerCase();
if (!query) return treeDocuments;
return treeDocuments.filter((d) => d.title.toLowerCase().includes(query));
}, [treeDocuments, debouncedSearch]);
if (!query) return treeDocumentsWithMemory;
return treeDocumentsWithMemory.filter((d) => d.title.toLowerCase().includes(query));
}, [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 counts: Partial<Record<string, number>> = {};
@ -1169,6 +1286,7 @@ function AuthenticatedDocumentsSidebarBase({
onCreateFolder={handleCreateFolder}
searchQuery={debouncedSearch.trim() || undefined}
onPreviewDocument={(doc) => {
if (openMemoryDocument(doc)) return;
openEditorPanel({
documentId: doc.id,
searchSpaceId,
@ -1176,6 +1294,7 @@ function AuthenticatedDocumentsSidebarBase({
});
}}
onEditDocument={(doc) => {
if (openMemoryDocument(doc)) return;
openEditorPanel({
documentId: doc.id,
searchSpaceId,
@ -1184,6 +1303,7 @@ function AuthenticatedDocumentsSidebarBase({
}}
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
onMoveDocument={handleMoveDocument}
onResetDocument={handleResetMemoryDocument}
onExportDocument={handleExportDocument}
onVersionHistory={(doc) => setVersionDocId(doc.id)}
activeTypes={activeTypes}

View file

@ -10,7 +10,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
import { BACKEND_URL, BACKEND_URL } from "@/lib/env-config";
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
interface DocumentContent {
@ -85,7 +85,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
try {
const url = new URL(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
);
url.searchParams.set("max_length", String(LARGE_DOCUMENT_THRESHOLD));
@ -143,7 +143,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
setSaving(true);
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
@ -285,7 +285,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
setDownloading(true);
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
{ method: "GET" }
);
if (!response.ok) throw new Error("Download failed");