diff --git a/surfsense_web/app/api/zero/query/route.ts b/surfsense_web/app/api/zero/query/route.ts
index a91edcd6f..8caac9cd4 100644
--- a/surfsense_web/app/api/zero/query/route.ts
+++ b/surfsense_web/app/api/zero/query/route.ts
@@ -4,11 +4,9 @@ import { NextResponse } from "next/server";
import type { Context } from "@/types/zero";
import { queries } from "@/zero/queries";
import { schema } from "@/zero/schema";
+import { BACKEND_URL } from "@/lib/env-config";
-const backendURL =
- process.env.FASTAPI_BACKEND_INTERNAL_URL ||
- process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ||
- "http://localhost:8000";
+const backendURL = BACKEND_URL;
async function authenticateRequest(
request: Request
diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
index d439cbb67..ecd5ab6b1 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
@@ -118,7 +118,7 @@ import {
trackChatResponseReceived,
} from "@/lib/posthog/events";
import Loading from "../loading";
-
+import { BACKEND_URL } from "@/lib/env-config";
const MobileEditorPanel = dynamic(
() =>
import("@/components/editor-panel/editor-panel").then((m) => ({
@@ -777,7 +777,7 @@ export default function NewChatPage() {
if (threadId) {
const token = getBearerToken();
if (token) {
- const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
+ const backendUrl = BACKEND_URL;
try {
const response = await fetch(
`${backendUrl}/api/v1/threads/${threadId}/cancel-active-turn`,
@@ -978,7 +978,7 @@ export default function NewChatPage() {
let streamBatcher: FrameBatchedUpdater | null = null;
try {
- const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
+ const backendUrl = BACKEND_URL;
const selection = await getAgentFilesystemSelection(searchSpaceId, {
localFilesystemEnabled,
});
@@ -1520,7 +1520,7 @@ export default function NewChatPage() {
}
try {
- const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
+ const backendUrl = BACKEND_URL;
const selection = await getAgentFilesystemSelection(searchSpaceId, {
localFilesystemEnabled,
});
diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout-shell.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout-shell.tsx
index 96d77d131..22f68edab 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout-shell.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/layout-shell.tsx
@@ -3,7 +3,6 @@
import {
BookText,
Bot,
- Brain,
CircleUser,
Earth,
ImageIcon,
@@ -27,7 +26,6 @@ export type SearchSpaceSettingsTab =
| "vision-models"
| "team-roles"
| "prompts"
- | "team-memory"
| "public-links";
const DEFAULT_TAB: SearchSpaceSettingsTab = "general";
@@ -89,11 +87,6 @@ export function SearchSpaceSettingsLayoutShell({
label: t("nav_system_instructions"),
icon:
,
},
- {
- value: "team-memory" as const,
- label: "Team Memory",
- icon:
,
- },
{
value: "public-links" as const,
label: t("nav_public_links"),
diff --git a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-memory/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-memory/page.tsx
deleted file mode 100644
index 0652b012e..000000000
--- a/surfsense_web/app/dashboard/[search_space_id]/search-space-settings/team-memory/page.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { TeamMemoryManager } from "@/components/settings/team-memory-manager";
-
-export default async function Page({ params }: { params: Promise<{ search_space_id: string }> }) {
- const { search_space_id } = await params;
- return
;
-}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx
deleted file mode 100644
index 3542f0925..000000000
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MemoryContent.tsx
+++ /dev/null
@@ -1,293 +0,0 @@
-"use client";
-
-import { useAtomValue } from "jotai";
-import { ArrowUp, ChevronDown, ClipboardCopy, Download, Info, Pencil } from "lucide-react";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { toast } from "sonner";
-import { z } from "zod";
-import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
-import { PlateEditor } from "@/components/editor/plate-editor";
-import { Alert, AlertDescription } from "@/components/ui/alert";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Spinner } from "@/components/ui/spinner";
-
-import { baseApiService } from "@/lib/apis/base-api.service";
-
-const MEMORY_HARD_LIMIT = 25_000;
-
-const MemoryReadSchema = z.object({
- memory_md: z.string(),
-});
-
-export function MemoryContent() {
- const activeSearchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
- const [memory, setMemory] = useState("");
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [editQuery, setEditQuery] = useState("");
- const [editing, setEditing] = useState(false);
- const [showInput, setShowInput] = useState(false);
- const textareaRef = useRef
(null);
- const inputContainerRef = useRef(null);
-
- const fetchMemory = useCallback(async () => {
- try {
- setLoading(true);
- const data = await baseApiService.get("/api/v1/users/me/memory", MemoryReadSchema);
- setMemory(data.memory_md);
- } catch {
- toast.error("Failed to load memory");
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- fetchMemory();
- }, [fetchMemory]);
-
- useEffect(() => {
- if (!showInput) return;
-
- const handlePointerDownOutside = (event: MouseEvent | TouchEvent) => {
- const target = event.target;
- if (!(target instanceof Node)) return;
- if (inputContainerRef.current?.contains(target)) return;
-
- setShowInput(false);
- };
-
- document.addEventListener("mousedown", handlePointerDownOutside);
- document.addEventListener("touchstart", handlePointerDownOutside, { passive: true });
-
- return () => {
- document.removeEventListener("mousedown", handlePointerDownOutside);
- document.removeEventListener("touchstart", handlePointerDownOutside);
- };
- }, [showInput]);
-
- const handleClear = async () => {
- try {
- setSaving(true);
- const data = await baseApiService.put("/api/v1/users/me/memory", MemoryReadSchema, {
- body: { memory_md: "" },
- });
- setMemory(data.memory_md);
- toast.success("Memory cleared");
- } catch {
- toast.error("Failed to clear memory");
- } finally {
- setSaving(false);
- }
- };
-
- const handleEdit = async () => {
- const query = editQuery.trim();
- if (!query) return;
-
- try {
- setEditing(true);
- const data = await baseApiService.post("/api/v1/users/me/memory/edit", MemoryReadSchema, {
- body: { query, search_space_id: Number(activeSearchSpaceId) },
- });
- setMemory(data.memory_md);
- setEditQuery("");
- setShowInput(false);
- toast.success("Memory updated");
- } catch {
- toast.error("Failed to edit memory");
- } finally {
- setEditing(false);
- }
- };
-
- const openInput = () => {
- setShowInput(true);
- requestAnimationFrame(() => textareaRef.current?.focus());
- };
-
- const handleDownload = () => {
- if (!memory) return;
- try {
- const blob = new Blob([memory], { type: "text/markdown;charset=utf-8" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = "personal-memory.md";
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- } catch {
- toast.error("Failed to download memory");
- }
- };
-
- const handleCopyMarkdown = async () => {
- if (!memory) return;
- try {
- await navigator.clipboard.writeText(memory);
- toast.success("Copied to clipboard");
- } catch {
- toast.error("Failed to copy memory");
- }
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- handleEdit();
- }
- };
-
- const displayMemory = memory.replace(/\(\d{4}-\d{2}-\d{2}\)\s*\[(fact|pref|instr)\]\s*/g, "");
- const charCount = memory.length;
-
- const getCounterColor = () => {
- if (charCount > MEMORY_HARD_LIMIT) return "text-red-500";
- if (charCount > 15_000) return "text-orange-500";
- if (charCount > 10_000) return "text-yellow-500";
- return "text-muted-foreground";
- };
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (!memory) {
- return (
-
-
What does SurfSense remember?
-
- Nothing yet. SurfSense picks up on your preferences and context as you chat.
-
-
- );
- }
-
- return (
-
-
-
-
-
- SurfSense uses this personal memory to personalize your responses across all
- conversations.
-
-
-
-
-
-
-
- {showInput ? (
-
-
-
setEditQuery(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="Tell SurfSense what to remember or forget"
- disabled={editing}
- className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/70"
- />
-
-
-
- ) : (
-
- )}
-
-
-
-
- {charCount.toLocaleString()} / {MEMORY_HARD_LIMIT.toLocaleString()}
- characters
- chars
- {charCount > 15_000 && charCount <= MEMORY_HARD_LIMIT && " - Approaching limit"}
- {charCount > MEMORY_HARD_LIMIT && " - Exceeds limit"}
-
-
-
-
-
-
-
-
-
-
- Copy as Markdown
-
-
-
- Download as Markdown
-
-
-
-
-
-
- );
-}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx
index 820021622..037568db3 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/layout-shell.tsx
@@ -1,7 +1,6 @@
"use client";
import {
- Brain,
CircleUser,
Keyboard,
KeyRound,
@@ -26,7 +25,6 @@ export type UserSettingsTab =
| "api-key"
| "prompts"
| "community-prompts"
- | "memory"
| "agent-permissions"
| "agent-status"
| "purchases"
@@ -75,11 +73,6 @@ export function UserSettingsLayoutShell({ searchSpaceId, children }: UserSetting
label: "Community Prompts",
icon: ,
},
- {
- value: "memory" as const,
- label: "Memory",
- icon: ,
- },
{
value: "agent-permissions" as const,
label: "Agent Permissions",
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/memory/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/memory/page.tsx
deleted file mode 100644
index b10c5bce5..000000000
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/memory/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MemoryContent } from "../components/MemoryContent";
-
-export default function Page() {
- return ;
-}
diff --git a/surfsense_web/atoms/editor/editor-panel.atom.ts b/surfsense_web/atoms/editor/editor-panel.atom.ts
index 28563e7d3..c302c66ee 100644
--- a/surfsense_web/atoms/editor/editor-panel.atom.ts
+++ b/surfsense_web/atoms/editor/editor-panel.atom.ts
@@ -3,10 +3,11 @@ import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right
interface EditorPanelState {
isOpen: boolean;
- kind: "document" | "local_file";
+ kind: "document" | "local_file" | "memory";
documentId: number | null;
localFilePath: string | null;
searchSpaceId: number | null;
+ memoryScope: "user" | "team" | null;
title: string | null;
}
@@ -16,6 +17,7 @@ const initialState: EditorPanelState = {
documentId: null,
localFilePath: null,
searchSpaceId: null,
+ memoryScope: null,
title: null,
};
@@ -38,6 +40,12 @@ export const openEditorPanelAtom = atom(
title?: string;
searchSpaceId?: number;
}
+ | {
+ kind: "memory";
+ memoryScope: "user" | "team";
+ title?: string;
+ searchSpaceId?: number;
+ }
) => {
if (!get(editorPanelAtom).isOpen) {
set(preEditorCollapsedAtom, get(rightPanelCollapsedAtom));
@@ -49,6 +57,21 @@ export const openEditorPanelAtom = atom(
documentId: null,
localFilePath: payload.localFilePath,
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,
});
set(rightPanelTabAtom, "editor");
@@ -61,6 +84,7 @@ export const openEditorPanelAtom = atom(
documentId: payload.documentId,
localFilePath: null,
searchSpaceId: payload.searchSpaceId,
+ memoryScope: null,
title: payload.title ?? null,
});
set(rightPanelTabAtom, "editor");
diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx
index ac1732441..f6c91e8bf 100644
--- a/surfsense_web/components/assistant-ui/assistant-message.tsx
+++ b/surfsense_web/components/assistant-ui/assistant-message.tsx
@@ -14,6 +14,7 @@ import {
ClipboardPaste,
CopyIcon,
DownloadIcon,
+ Dot,
ExternalLink,
Globe,
MessageCircleReply,
@@ -330,9 +331,14 @@ const MessageInfoDropdown: FC<{ chatTurnId: string | null | undefined }> = ({ ch
{icon}
{name}
-
- {counts.total_tokens.toLocaleString()} tokens
- {costMicros && costMicros > 0 ? ` · ${formatTurnCost(costMicros)}` : ""}
+
+ {counts.total_tokens.toLocaleString()} tokens
+ {costMicros && costMicros > 0 ? (
+ <>
+
+ {formatTurnCost(costMicros)}
+ >
+ ) : null}
);
@@ -342,11 +348,14 @@ const MessageInfoDropdown: FC<{ chatTurnId: string | null | undefined }> = ({ ch
className="focus:bg-accent focus:text-accent-foreground relative flex cursor-default flex-col items-start gap-0.5 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none"
onSelect={(e) => e.preventDefault()}
>
-
- {usage.total_tokens.toLocaleString()} tokens
- {usage.cost_micros && usage.cost_micros > 0
- ? ` · ${formatTurnCost(usage.cost_micros)}`
- : ""}
+
+ {usage.total_tokens.toLocaleString()} tokens
+ {usage.cost_micros && usage.cost_micros > 0 ? (
+ <>
+
+ {formatTurnCost(usage.cost_micros)}
+ >
+ ) : null}
)}
diff --git a/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.json b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.json
index b4e85eab0..466446da9 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.json
+++ b/surfsense_web/components/assistant-ui/connector-popup/config/connector-status-config.json
@@ -19,6 +19,11 @@
"enabled": false,
"status": "maintenance",
"statusMessage": "Rework in progress."
+ },
+ "COMPOSIO_GOOGLE_DRIVE_CONNECTOR": {
+ "enabled": false,
+ "status": "maintenance",
+ "statusMessage": "Temporarily unavailable due to an upstream Composio bug (ComposioHQ/composio#3471) that returns malformed presigned URLs for Drive file downloads. Use the native Google Drive connector in the meantime."
}
},
"globalSettings": {
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx
index 695e97d7b..01b86a538 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx
@@ -8,10 +8,12 @@ import { EnumConnectorName } from "@/contracts/enums/connector";
import { useApiKey } from "@/hooks/use-api-key";
import { getConnectorBenefits } from "../connector-benefits";
import type { ConnectFormProps } from "../index";
+import { BACKEND_URL } from "@/lib/env-config";
const PLUGIN_RELEASES_URL =
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
+
/**
* Obsidian connect form for the plugin-only architecture.
*
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx
index af818671a..fe6724ed8 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx
@@ -10,7 +10,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { authenticatedFetch } from "@/lib/auth-utils";
import type { ConnectorConfigProps } from "../index";
-
+import { BACKEND_URL } from "@/lib/env-config";
export interface CirclebackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
@@ -42,7 +42,7 @@ export const CirclebackConfig: FC = ({ connector, onNameC
const doFetch = async () => {
if (!connector.search_space_id) return;
- const baseUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL;
+ const baseUrl = BACKEND_URL;
if (!baseUrl) {
console.error("NEXT_PUBLIC_FASTAPI_BACKEND_URL is not configured");
setIsLoading(false);
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx
index b64f97414..90b28dd1a 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx
@@ -21,7 +21,7 @@ import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../../constants/connect
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
import { MCPServiceConfig } from "../components/mcp-service-config";
import { getConnectorConfigComponent } from "../index";
-
+import { BACKEND_URL } from "@/lib/env-config";
const VISION_LLM_CONNECTOR_TYPES = new Set([
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
@@ -94,7 +94,7 @@ export const ConnectorEditView: FC = ({
if (!spaceId || !reauthEndpoint) return;
setReauthing(true);
try {
- const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
+ const backendUrl = BACKEND_URL;
const url = new URL(`${backendUrl}${reauthEndpoint}`);
url.searchParams.set("connector_id", String(connector.id));
url.searchParams.set("space_id", String(spaceId));
diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts
index b49bfda96..d1d675ad1 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts
+++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts
@@ -43,7 +43,7 @@ import {
parseOAuthAuthResponse,
validateIndexingConfigState,
} from "../constants/connector-popup.schemas";
-
+import { BACKEND_URL } from "@/lib/env-config";
const OAUTH_RESULT_COOKIE = "connector_oauth_result";
function readOAuthResultCookie(): string | null {
@@ -364,7 +364,7 @@ export const useConnectorDialog = () => {
try {
// Check if authEndpoint already has query parameters
const separator = connector.authEndpoint.includes("?") ? "&" : "?";
- const url = `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}${separator}space_id=${searchSpaceId}`;
+ const url = `${BACKEND_URL}${connector.authEndpoint}${separator}space_id=${searchSpaceId}`;
const response = await authenticatedFetch(url, { method: "GET" });
diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx
index 65960db22..41dae221e 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx
@@ -16,7 +16,7 @@ import { cn } from "@/lib/utils";
import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
import { useConnectorStatus } from "../hooks/use-connector-status";
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
-
+import { BACKEND_URL } from "@/lib/env-config";
interface ConnectorAccountsListViewProps {
connectorType: string;
connectorTitle: string;
@@ -59,7 +59,7 @@ export const ConnectorAccountsListView: FC = ({
if (!searchSpaceId || !endpoint) return;
setReauthingId(connector.id);
try {
- const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
+ const backendUrl = BACKEND_URL;
const url = new URL(`${backendUrl}${endpoint}`);
url.searchParams.set("connector_id", String(connector.id));
url.searchParams.set("space_id", String(searchSpaceId));
diff --git a/surfsense_web/components/documents/DocumentNode.tsx b/surfsense_web/components/documents/DocumentNode.tsx
index 0f3cd4a19..a13bd0079 100644
--- a/surfsense_web/components/documents/DocumentNode.tsx
+++ b/surfsense_web/components/documents/DocumentNode.tsx
@@ -9,6 +9,7 @@ import {
MoreHorizontal,
Move,
Pencil,
+ RotateCcw,
Trash2,
} from "lucide-react";
import React, { useCallback, useRef, useState } from "react";
@@ -61,8 +62,13 @@ interface DocumentNodeProps {
onEdit: (doc: DocumentNodeDoc) => void;
onDelete: (doc: DocumentNodeDoc) => void;
onMove: (doc: DocumentNodeDoc) => void;
+ onReset?: (doc: DocumentNodeDoc) => void;
onExport?: (doc: DocumentNodeDoc, format: string) => void;
onVersionHistory?: (doc: DocumentNodeDoc) => void;
+ canDelete?: boolean;
+ canMove?: boolean;
+ canMention?: boolean;
+ canEdit?: boolean;
contextMenuOpen?: boolean;
onContextMenuOpenChange?: (open: boolean) => void;
}
@@ -76,8 +82,13 @@ export const DocumentNode = React.memo(function DocumentNode({
onEdit,
onDelete,
onMove,
+ onReset,
onExport,
onVersionHistory,
+ canDelete = true,
+ canMove = true,
+ canMention = true,
+ canEdit = true,
contextMenuOpen,
onContextMenuOpenChange,
}: DocumentNodeProps) {
@@ -85,8 +96,13 @@ export const DocumentNode = React.memo(function DocumentNode({
const isFailed = statusState === "failed";
const isProcessing = statusState === "pending" || statusState === "processing";
const isUnavailable = isProcessing || isFailed;
- const isSelectable = !isUnavailable;
- const isEditable = EDITABLE_DOCUMENT_TYPES.has(doc.document_type) && !isUnavailable;
+ const isMemoryDocument =
+ 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(() => {
if (isSelectable) {
@@ -94,13 +110,22 @@ export const DocumentNode = React.memo(function DocumentNode({
}
}, [doc, isMentioned, isSelectable, onToggleChatMention]);
+ const handlePrimaryClick = useCallback(() => {
+ if (canMention) {
+ handleCheckChange();
+ return;
+ }
+ onPreview(doc);
+ }, [canMention, doc, handleCheckChange, onPreview]);
+
const [{ isDragging }, drag] = useDrag(
() => ({
type: DND_TYPES.DOCUMENT,
item: { id: doc.id },
+ canDrag: canMove,
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
}),
- [doc.id]
+ [canMove, doc.id]
);
const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -130,9 +155,11 @@ export const DocumentNode = React.memo(function DocumentNode({
const attachRef = useCallback(
(node: HTMLDivElement | null) => {
(rowRef as React.MutableRefObject).current = node;
- drag(node);
+ if (canMove) {
+ drag(node);
+ }
},
- [drag]
+ [canMove, drag]
);
return (
@@ -187,12 +214,32 @@ export const DocumentNode = React.memo(function DocumentNode({
);
}
return (
- e.stopPropagation()}
- className="h-3.5 w-3.5 shrink-0"
- />
+ <>
+ {isMemoryDocument ? (
+
+
+
+ ) : canMention ? (
+ e.stopPropagation()}
+ className="h-3.5 w-3.5 shrink-0"
+ />
+ ) : (
+
+ {getDocumentTypeIcon(
+ doc.document_type as DocumentTypeEnum,
+ "h-3.5 w-3.5 text-muted-foreground"
+ )}
+
+ )}
+ >
);
})()}
@@ -205,8 +252,8 @@ export const DocumentNode = React.memo(function DocumentNode({