feat: implement memory document fetching and saving functionality in the editor panel, and remove deprecated memory hook

This commit is contained in:
Anish Sarkar 2026-05-20 12:50:15 +05:30
parent 61234e125f
commit 78a3c71bb5
4 changed files with 140 additions and 203 deletions

View file

@ -216,13 +216,9 @@ export const DocumentNode = React.memo(function DocumentNode({
return ( return (
<> <>
{isMemoryDocument ? ( {isMemoryDocument ? (
<button <span
type="button"
aria-disabled="true" aria-disabled="true"
tabIndex={-1}
className="h-3.5 w-3.5 shrink-0 cursor-default" className="h-3.5 w-3.5 shrink-0 cursor-default"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
> >
<Checkbox <Checkbox
checked={false} checked={false}
@ -230,7 +226,7 @@ export const DocumentNode = React.memo(function DocumentNode({
aria-disabled aria-disabled
className="h-3.5 w-3.5 pointer-events-none" className="h-3.5 w-3.5 pointer-events-none"
/> />
</button> </span>
) : canMention ? ( ) : canMention ? (
<Checkbox <Checkbox
checked={isMentioned} checked={isMentioned}

View file

@ -16,6 +16,12 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { VersionHistoryButton } from "@/components/documents/version-history"; import { VersionHistoryButton } from "@/components/documents/version-history";
import {
fetchMemoryEditorDocument,
getMemoryLimitState,
type MemoryLimits,
saveMemoryMarkdown,
} from "@/components/editor-panel/memory";
import { SourceCodeEditor } from "@/components/editor/source-code-editor"; import { SourceCodeEditor } from "@/components/editor/source-code-editor";
import { MarkdownViewer } from "@/components/markdown-viewer"; import { MarkdownViewer } from "@/components/markdown-viewer";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
@ -24,7 +30,6 @@ import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/u
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { useMediaQuery } from "@/hooks/use-media-query"; import { useMediaQuery } from "@/hooks/use-media-query";
import { getMemoryLimitState, type MemoryLimits } from "@/hooks/use-memory";
import { useElectronAPI } from "@/hooks/use-platform"; import { useElectronAPI } from "@/hooks/use-platform";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
import { inferMonacoLanguageFromPath } from "@/lib/editor-language"; import { inferMonacoLanguageFromPath } from "@/lib/editor-language";
@ -207,35 +212,16 @@ export function EditorPanelContent({
return; return;
} }
if (isMemoryMode) { if (isMemoryMode) {
if (memoryScope === "team" && !searchSpaceId) { if (!memoryScope) throw new Error("Missing memory context");
throw new Error("Missing search space context"); const { document, limits } = await fetchMemoryEditorDocument({
} scope: memoryScope,
const response = await authenticatedFetch( searchSpaceId,
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${ title,
memoryScope === "team" signal: controller.signal,
? `/api/v1/searchspaces/${searchSpaceId}/memory` });
: "/api/v1/users/me/memory"
}`,
{ method: "GET" }
);
if (controller.signal.aborted) return; if (controller.signal.aborted) return;
if (!response.ok) { setMemoryLimits(limits);
const errorData = await response const content: EditorContent = document;
.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;
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 ?? "",
};
markdownRef.current = content.source_markdown; markdownRef.current = content.source_markdown;
setDisplayTitle(content.title); setDisplayTitle(content.title);
setEditorDoc(content); setEditorDoc(content);
@ -370,34 +356,14 @@ export function EditorPanelContent({
return true; return true;
} }
if (isMemoryMode) { if (isMemoryMode) {
if (memoryScope === "team" && !searchSpaceId) { if (!memoryScope) throw new Error("Missing memory context");
throw new Error("Missing search space context"); const { markdown: savedContent, limits } = await saveMemoryMarkdown({
} scope: memoryScope,
const response = await authenticatedFetch( searchSpaceId,
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${ markdown: markdownRef.current,
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;
markdownRef.current = savedContent; markdownRef.current = savedContent;
setMemoryLimits(data.limits ?? memoryLimits); setMemoryLimits(limits ?? memoryLimits);
setEditorDoc((prev) => (prev ? { ...prev, source_markdown: savedContent } : prev)); setEditorDoc((prev) => (prev ? { ...prev, source_markdown: savedContent } : prev));
setEditedMarkdown(null); setEditedMarkdown(null);
if (!options?.silent) { if (!options?.silent) {

View file

@ -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,
};
}

View file

@ -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<typeof MemoryLimitsSchema>;
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<MemoryLimits | null>(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 });
}