mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
feat: implement memory document fetching and saving functionality in the editor panel, and remove deprecated memory hook
This commit is contained in:
parent
61234e125f
commit
78a3c71bb5
4 changed files with 140 additions and 203 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
116
surfsense_web/components/editor-panel/memory.ts
Normal file
116
surfsense_web/components/editor-panel/memory.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue