diff --git a/surfsense_web/components/documents/DocumentNode.tsx b/surfsense_web/components/documents/DocumentNode.tsx
index a5b02cbb3..86c9b899a 100644
--- a/surfsense_web/components/documents/DocumentNode.tsx
+++ b/surfsense_web/components/documents/DocumentNode.tsx
@@ -216,13 +216,9 @@ export const DocumentNode = React.memo(function DocumentNode({
return (
<>
{isMemoryDocument ? (
-
+
) : canMention ? (
({ detail: "Failed to fetch memory" }));
- throw new Error(errorData.detail || "Failed to fetch memory");
- }
- const data = (await response.json()) as {
- memory_md?: string;
- limits?: MemoryLimits;
- };
- setMemoryLimits(data.limits ?? null);
- 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 ?? "",
- };
+ setMemoryLimits(limits);
+ const content: EditorContent = document;
markdownRef.current = content.source_markdown;
setDisplayTitle(content.title);
setEditorDoc(content);
@@ -370,34 +356,14 @@ export function EditorPanelContent({
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;
- limits?: MemoryLimits;
- };
- const savedContent = data.memory_md ?? markdownRef.current;
+ if (!memoryScope) throw new Error("Missing memory context");
+ const { markdown: savedContent, limits } = await saveMemoryMarkdown({
+ scope: memoryScope,
+ searchSpaceId,
+ markdown: markdownRef.current,
+ });
markdownRef.current = savedContent;
- setMemoryLimits(data.limits ?? memoryLimits);
+ setMemoryLimits(limits ?? memoryLimits);
setEditorDoc((prev) => (prev ? { ...prev, source_markdown: savedContent } : prev));
setEditedMarkdown(null);
if (!options?.silent) {
diff --git a/surfsense_web/components/editor-panel/memory.ts b/surfsense_web/components/editor-panel/memory.ts
new file mode 100644
index 000000000..aa5b1f68d
--- /dev/null
+++ b/surfsense_web/components/editor-panel/memory.ts
@@ -0,0 +1,116 @@
+"use client";
+
+import { authenticatedFetch } from "@/lib/auth-utils";
+
+export type MemoryScope = "user" | "team";
+
+export interface MemoryLimits {
+ soft: number;
+ hard: number;
+}
+
+export type MemoryLimitLevel = "ok" | "warning" | "error";
+
+export interface MemoryEditorDocument {
+ document_id: number;
+ title: string;
+ document_type: "USER_MEMORY" | "TEAM_MEMORY";
+ source_markdown: string;
+}
+
+interface MemoryReadResponse {
+ memory_md?: string;
+ limits?: MemoryLimits;
+}
+
+function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) {
+ if (scope === "user") return "/api/v1/users/me/memory";
+ if (!searchSpaceId) throw new Error("Missing search space context");
+ return `/api/v1/searchspaces/${searchSpaceId}/memory`;
+}
+
+function getBackendUrl(path: string) {
+ return `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${path}`;
+}
+
+export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {
+ if (!limits) {
+ return {
+ level: "ok" as MemoryLimitLevel,
+ label: `${length.toLocaleString()} chars`,
+ isOverLimit: false,
+ };
+ }
+
+ const isOverLimit = length > limits.hard;
+ const isNearLimit = length > limits.soft;
+ const level: MemoryLimitLevel = isOverLimit ? "error" : isNearLimit ? "warning" : "ok";
+ const suffix = isOverLimit ? " - Exceeds limit" : isNearLimit ? " - Approaching limit" : "";
+
+ return {
+ level,
+ label: `${length.toLocaleString()}/${limits.hard.toLocaleString()} chars${suffix}`,
+ isOverLimit,
+ };
+}
+
+export async function fetchMemoryEditorDocument({
+ scope,
+ searchSpaceId,
+ title,
+ signal,
+}: {
+ scope: MemoryScope;
+ searchSpaceId?: number | null;
+ title?: string | null;
+ signal?: AbortSignal;
+}) {
+ const response = await authenticatedFetch(getBackendUrl(getMemoryPath(scope, searchSpaceId)), {
+ method: "GET",
+ signal,
+ });
+ 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 MemoryReadResponse;
+ const isTeamMemory = scope === "team";
+
+ return {
+ limits: data.limits ?? null,
+ document: {
+ document_id: isTeamMemory ? -1002 : -1001,
+ title: title || (isTeamMemory ? "Team Memory" : "Personal Memory"),
+ document_type: isTeamMemory ? "TEAM_MEMORY" : "USER_MEMORY",
+ source_markdown: data.memory_md ?? "",
+ } satisfies MemoryEditorDocument,
+ };
+}
+
+export async function saveMemoryMarkdown({
+ scope,
+ searchSpaceId,
+ markdown,
+}: {
+ scope: MemoryScope;
+ searchSpaceId?: number | null;
+ markdown: string;
+}) {
+ const response = await authenticatedFetch(getBackendUrl(getMemoryPath(scope, searchSpaceId)), {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ memory_md: markdown }),
+ });
+ 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 MemoryReadResponse;
+
+ return {
+ markdown: data.memory_md ?? markdown,
+ limits: data.limits,
+ };
+}
diff --git a/surfsense_web/hooks/use-memory.ts b/surfsense_web/hooks/use-memory.ts
deleted file mode 100644
index 609aad537..000000000
--- a/surfsense_web/hooks/use-memory.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useState } from "react";
-import { z } from "zod";
-import { baseApiService } from "@/lib/apis/base-api.service";
-
-const MemoryLimitsSchema = z.object({
- soft: z.number(),
- hard: z.number(),
-});
-
-const MemoryReadSchema = z.object({
- memory_md: z.string(),
- limits: MemoryLimitsSchema,
-});
-
-type MemoryScope = "user" | "team";
-export type MemoryLimits = z.infer;
-export type MemoryLimitLevel = "ok" | "warning" | "error";
-
-interface UseMemoryOptions {
- scope: MemoryScope;
- searchSpaceId?: number | null;
- autoLoad?: boolean;
-}
-
-function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) {
- if (scope === "user") return "/api/v1/users/me/memory";
- if (!searchSpaceId) throw new Error("searchSpaceId is required for team memory");
- return `/api/v1/searchspaces/${searchSpaceId}/memory`;
-}
-
-export function stripMemoryDisplayPrefixes(memory: string) {
- return memory.replace(
- /^\s*-\s+(?:\(\d{4}-\d{2}-\d{2}\)\s*\[(?:fact|pref|instr)\]\s*|\d{4}-\d{2}-\d{2}:\s*)/gim,
- "- "
- );
-}
-
-export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {
- if (!limits) {
- return {
- level: "ok" as MemoryLimitLevel,
- label: `${length.toLocaleString()} chars`,
- isOverLimit: false,
- };
- }
-
- const isOverLimit = length > limits.hard;
- const isNearLimit = length > limits.soft;
- const level: MemoryLimitLevel = isOverLimit ? "error" : isNearLimit ? "warning" : "ok";
- const suffix = isOverLimit ? " - Exceeds limit" : isNearLimit ? " - Approaching limit" : "";
-
- return {
- level,
- label: `${length.toLocaleString()}/${limits.hard.toLocaleString()} chars${suffix}`,
- isOverLimit,
- };
-}
-
-export function useMemory({ scope, searchSpaceId, autoLoad = true }: UseMemoryOptions) {
- const [memory, setMemory] = useState("");
- const [limits, setLimits] = useState(null);
- const [loading, setLoading] = useState(autoLoad);
- const [saving, setSaving] = useState(false);
-
- const load = useCallback(async () => {
- setLoading(true);
- try {
- const data = await baseApiService.get(getMemoryPath(scope, searchSpaceId), MemoryReadSchema);
- setMemory(data.memory_md);
- setLimits(data.limits);
- return data.memory_md;
- } finally {
- setLoading(false);
- }
- }, [scope, searchSpaceId]);
-
- useEffect(() => {
- if (!autoLoad) return;
- load().catch(() => {
- setLoading(false);
- });
- }, [autoLoad, load]);
-
- const save = useCallback(
- async (memoryMd: string) => {
- setSaving(true);
- try {
- const data = await baseApiService.put(
- getMemoryPath(scope, searchSpaceId),
- MemoryReadSchema,
- {
- body: { memory_md: memoryMd },
- }
- );
- setMemory(data.memory_md);
- setLimits(data.limits);
- return data.memory_md;
- } finally {
- setSaving(false);
- }
- },
- [scope, searchSpaceId]
- );
-
- const reset = useCallback(async () => {
- setSaving(true);
- try {
- const data = await baseApiService.post(
- `${getMemoryPath(scope, searchSpaceId)}/reset`,
- MemoryReadSchema
- );
- setMemory(data.memory_md);
- setLimits(data.limits);
- return data.memory_md;
- } finally {
- setSaving(false);
- }
- }, [scope, searchSpaceId]);
-
- return {
- memory,
- setMemory,
- limits,
- displayMemory: stripMemoryDisplayPrefixes(memory),
- loading,
- saving,
- load,
- save,
- reset,
- };
-}
-
-export function useUserMemory(searchSpaceId?: number | null) {
- return useMemory({ scope: "user", searchSpaceId });
-}
-
-export function useTeamMemory(searchSpaceId?: number | null) {
- return useMemory({ scope: "team", searchSpaceId });
-}