From 0c0bd22185fbc3219386e71a0350521fc8a89add Mon Sep 17 00:00:00 2001 From: Ashwin-3cS Date: Sat, 28 Mar 2026 20:44:32 +0530 Subject: [PATCH 1/4] feat(noter): wire memory detection panel + Walruscan explorer links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire up existing but unused MemoryPanelEnhanced and MemoryDetectButton components into the NoteEditor, completing the detect → approve → save memory workflow. - Register MemoryHighlightNode + MemoryHighlightPlugin in editor config - Add "Detect Memories" button to toolbar alongside existing Save button - Render MemoryPanelEnhanced as right sidebar for memory approval flow - Add fuzzy text matching in MemoryHighlightPlugin to handle LLM-rephrased facts that don't match editor text verbatim (longest common substring) - Add /api/memory/remember-one endpoint for individual memory saves (existing /api/memory/remember handles bulk analyze, unchanged) - Replace TODO Sui Explorer placeholder with direct Walruscan blob links - Fix memory-hover-preview to use direct link instead of toast --- .../app/api/memory/remember-one/route.ts | 30 +++++++++++++++ .../feature/note/hook/use-note-memory-save.ts | 2 +- .../note/plugins/MemoryHighlightPlugin.tsx | 37 ++++++++++++++++++- .../feature/note/ui/memory-hover-preview.tsx | 14 +++---- .../feature/note/ui/memory-panel-enhanced.tsx | 24 ++++++------ .../package/feature/note/ui/note-editor.tsx | 9 ++++- 6 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 apps/noter/app/api/memory/remember-one/route.ts diff --git a/apps/noter/app/api/memory/remember-one/route.ts b/apps/noter/app/api/memory/remember-one/route.ts new file mode 100644 index 00000000..465d4c53 --- /dev/null +++ b/apps/noter/app/api/memory/remember-one/route.ts @@ -0,0 +1,30 @@ +/** + * Memory Remember One API — saves a single approved memory text to MemWal. + * Used by the memory panel's "Approve" flow (individual memory saves). + * Returns { id, blob_id, owner, namespace } for blockchain data display. + */ + +import { rememberText } from "@/feature/note/lib/pdw-client"; + +export async function POST(req: Request) { + try { + const { text } = await req.json(); + + if (!text || typeof text !== "string") { + return Response.json({ error: "text is required" }, { status: 400 }); + } + + if (text.trim().length < 10) { + return Response.json({ error: "Text too short to remember" }, { status: 400 }); + } + + const result = await rememberText(text); + return Response.json(result); + } catch (error) { + console.error("[memory/remember-one] Error:", error); + return Response.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ); + } +} diff --git a/apps/noter/package/feature/note/hook/use-note-memory-save.ts b/apps/noter/package/feature/note/hook/use-note-memory-save.ts index 5586ae2e..4a414260 100644 --- a/apps/noter/package/feature/note/hook/use-note-memory-save.ts +++ b/apps/noter/package/feature/note/hook/use-note-memory-save.ts @@ -48,7 +48,7 @@ export function useNoteMemorySave() { }); // Call server-side API to remember (v2 SDK) - const response = await fetch("/api/memory/remember", { + const response = await fetch("/api/memory/remember-one", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: memoryText }), diff --git a/apps/noter/package/feature/note/plugins/MemoryHighlightPlugin.tsx b/apps/noter/package/feature/note/plugins/MemoryHighlightPlugin.tsx index aeb6900a..8e99416a 100644 --- a/apps/noter/package/feature/note/plugins/MemoryHighlightPlugin.tsx +++ b/apps/noter/package/feature/note/plugins/MemoryHighlightPlugin.tsx @@ -154,7 +154,42 @@ export function MemoryHighlightPlugin({ if (index === -1) { index = findUnusedOccurrence(trimmedMemoryText); if (index !== -1) { - matchedText = trimmedMemoryText; // We matched the trimmed version! + matchedText = trimmedMemoryText; + } + } + + // If that fails, try fuzzy match: case-insensitive search, + // then find the best overlapping substring between LLM text and editor text + if (index === -1) { + const lowerFull = fullText.toLowerCase(); + const lowerMemory = trimmedMemoryText.toLowerCase(); + + // Strategy 1: case-insensitive exact match + const ciIndex = lowerFull.indexOf(lowerMemory); + if (ciIndex !== -1 && !usedPositions.has(ciIndex)) { + index = ciIndex; + matchedText = fullText.slice(ciIndex, ciIndex + trimmedMemoryText.length); + } + + // Strategy 2: find longest common substring (min 10 chars) + if (index === -1) { + let best = { start: -1, len: 0 }; + for (let i = 0; i < lowerFull.length; i++) { + for (let j = 0; j < lowerMemory.length; j++) { + if (lowerFull[i] !== lowerMemory[j]) continue; + let k = 0; + while (i + k < lowerFull.length && j + k < lowerMemory.length && lowerFull[i + k] === lowerMemory[j + k]) { + k++; + } + if (k > best.len) { + best = { start: i, len: k }; + } + } + } + if (best.len >= 10 && !usedPositions.has(best.start)) { + index = best.start; + matchedText = fullText.slice(best.start, best.start + best.len); + } } } diff --git a/apps/noter/package/feature/note/ui/memory-hover-preview.tsx b/apps/noter/package/feature/note/ui/memory-hover-preview.tsx index 465f612e..1f36c4e6 100644 --- a/apps/noter/package/feature/note/ui/memory-hover-preview.tsx +++ b/apps/noter/package/feature/note/ui/memory-hover-preview.tsx @@ -12,6 +12,7 @@ import { Card, CardContent } from "@/shared/components/ui/card"; import { Separator } from "@/shared/components/ui/separator"; import type { MemoryData } from "@/shared/db/type"; import { cn } from "@/shared/lib/utils"; +import { ZKLOGIN_CONFIG } from "@/feature/auth/constant"; import { AlertCircle, CheckCircle2, @@ -205,16 +206,15 @@ export function MemoryHoverPreview({ {memory.memwalMemoryId.slice(0, 16)}... - + {/* Blob ID */} diff --git a/apps/noter/package/feature/note/ui/memory-panel-enhanced.tsx b/apps/noter/package/feature/note/ui/memory-panel-enhanced.tsx index 6e6e5f33..527fe2bb 100644 --- a/apps/noter/package/feature/note/ui/memory-panel-enhanced.tsx +++ b/apps/noter/package/feature/note/ui/memory-panel-enhanced.tsx @@ -36,6 +36,7 @@ import { import { forwardRef, useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { useNoteMemorySave } from "../hook/use-note-memory-save"; +import { ZKLOGIN_CONFIG } from "@/feature/auth/constant"; import { $getAllMemories, REMOVE_MEMORY_HIGHLIGHT_COMMAND, UPDATE_MEMORY_STATUS_COMMAND } from "../plugins/MemoryHighlightPlugin"; const STATUS_CONFIGS = { @@ -433,18 +434,17 @@ const MemoryCard = forwardRef(
Memory: {memory.memwalMemoryId.slice(0, 12)}... - + {memory.memwalBlobId && ( + e.stopPropagation()} + className="shrink-0 p-0.5 text-blue-600 dark:text-blue-400 hover:underline" + > + + + )}
{memory.memwalBlobId && (
Blob: {memory.memwalBlobId.slice(0, 12)}...
diff --git a/apps/noter/package/feature/note/ui/note-editor.tsx b/apps/noter/package/feature/note/ui/note-editor.tsx index b8a41d41..595c1e4b 100644 --- a/apps/noter/package/feature/note/ui/note-editor.tsx +++ b/apps/noter/package/feature/note/ui/note-editor.tsx @@ -34,6 +34,10 @@ import { MarketingBorder } from "@/package/shared/components/border"; import { Button } from "@/shared/components/ui/button"; import { Brain, Check, Loader2, Save } from "lucide-react"; import { useNote } from "../hook/use-note"; +import { MemoryHighlightNode } from "../nodes/MemoryHighlightNode"; +import { MemoryHighlightPlugin } from "../plugins/MemoryHighlightPlugin"; +import { MemoryPanelEnhanced } from "./memory-panel-enhanced"; +import { MemoryDetectButton } from "./memory-detect-button"; // ════════════════════════════════════════════════════════════════════════════ // EDITOR CONFIG @@ -54,6 +58,7 @@ const editorConfig = { TableCellNode, TableRowNode, HorizontalRuleNode, + MemoryHighlightNode, ], onError(error: Error) { console.error("Lexical error:", error); @@ -195,6 +200,7 @@ export function NoteEditor({ noteId }: NoteEditorProps) { {/* Toolbar */}
+
{/* Editor */} @@ -220,10 +226,11 @@ export function NoteEditor({ noteId }: NoteEditorProps) { - +
+ ); From 5c274f84929e1b8b996e4bb0cc4768cfb52ac614 Mon Sep 17 00:00:00 2001 From: Ashwin-3cS Date: Sat, 28 Mar 2026 20:59:24 +0530 Subject: [PATCH 2/4] fix(noter): use Walruscan blob link in memory hover preview The hover preview was linking to Suiscan with the vector DB UUID (memwalMemoryId), which isn't a Sui object. Changed to Walruscan blob URL using memwalBlobId, matching the panel component fix. --- apps/noter/package/feature/note/ui/memory-hover-preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/noter/package/feature/note/ui/memory-hover-preview.tsx b/apps/noter/package/feature/note/ui/memory-hover-preview.tsx index 1f36c4e6..fb5c7714 100644 --- a/apps/noter/package/feature/note/ui/memory-hover-preview.tsx +++ b/apps/noter/package/feature/note/ui/memory-hover-preview.tsx @@ -207,7 +207,7 @@ export function MemoryHoverPreview({ {memory.memwalMemoryId.slice(0, 16)}... e.stopPropagation()} From 265b4b784ee0069454a3f0a3fb3bb4e750aaec33 Mon Sep 17 00:00:00 2001 From: Ashwin-3cS Date: Thu, 2 Apr 2026 16:14:28 +0530 Subject: [PATCH 3/4] fix(noter): guard Walruscan link on memwalBlobId in memory-hover-preview --- .../feature/note/ui/memory-hover-preview.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/noter/package/feature/note/ui/memory-hover-preview.tsx b/apps/noter/package/feature/note/ui/memory-hover-preview.tsx index fb5c7714..8d16b7b4 100644 --- a/apps/noter/package/feature/note/ui/memory-hover-preview.tsx +++ b/apps/noter/package/feature/note/ui/memory-hover-preview.tsx @@ -206,15 +206,17 @@ export function MemoryHoverPreview({ {memory.memwalMemoryId.slice(0, 16)}... - e.stopPropagation()} - className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline" - > - - + {memory.memwalBlobId && ( + e.stopPropagation()} + className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline" + > + + + )} {/* Blob ID */} From 9be89a6310c347d839e0ebbd8a88fe831e689f67 Mon Sep 17 00:00:00 2001 From: Ashwin-3cS Date: Thu, 14 May 2026 03:21:36 +0530 Subject: [PATCH 4/4] fix(noter): resolve user session in remember-one approve flow --- .../app/api/memory/remember-one/route.ts | 82 ++++++++++++++----- .../feature/note/hook/use-note-memory-save.ts | 23 +++++- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/apps/noter/app/api/memory/remember-one/route.ts b/apps/noter/app/api/memory/remember-one/route.ts index 465d4c53..8126455a 100644 --- a/apps/noter/app/api/memory/remember-one/route.ts +++ b/apps/noter/app/api/memory/remember-one/route.ts @@ -5,26 +5,70 @@ */ import { rememberText } from "@/feature/note/lib/pdw-client"; +import { db } from "@/shared/lib/db"; +import { zkLoginSessions, walletSessions, users } from "@/shared/db/schema"; +import { eq } from "drizzle-orm"; + +async function resolveUserKey(req: Request) { + const sessionId = req.headers.get("x-session-id"); + if (!sessionId) return { key: null, accountId: null }; + + const [session] = await db + .select() + .from(walletSessions) + .where(eq(walletSessions.id, sessionId)) + .limit(1); + + if (!session?.userId || session.expiresAt < new Date()) { + const [zkSession] = await db + .select() + .from(zkLoginSessions) + .where(eq(zkLoginSessions.id, sessionId)) + .limit(1); + if (!zkSession?.userId || zkSession.expiresAt < new Date()) { + return { key: null, accountId: null }; + } + const [user] = await db.select().from(users).where(eq(users.id, zkSession.userId)).limit(1); + return { + key: user?.delegatePrivateKey ?? null, + accountId: user?.delegateAccountId ?? null, + }; + } + + const [user] = await db.select().from(users).where(eq(users.id, session.userId)).limit(1); + return { + key: user?.delegatePrivateKey ?? null, + accountId: user?.delegateAccountId ?? null, + }; +} export async function POST(req: Request) { - try { - const { text } = await req.json(); - - if (!text || typeof text !== "string") { - return Response.json({ error: "text is required" }, { status: 400 }); - } - - if (text.trim().length < 10) { - return Response.json({ error: "Text too short to remember" }, { status: 400 }); - } - - const result = await rememberText(text); - return Response.json(result); - } catch (error) { - console.error("[memory/remember-one] Error:", error); - return Response.json( - { error: error instanceof Error ? error.message : "Unknown error" }, - { status: 500 } - ); + try { + const { text } = await req.json(); + + if (!text || typeof text !== "string") { + return Response.json({ error: "text is required" }, { status: 400 }); + } + + if (text.trim().length < 10) { + return Response.json({ error: "Text too short to remember" }, { status: 400 }); } + + const { key, accountId } = await resolveUserKey(req); + if ((!key || !accountId) && (!process.env.MEMWAL_KEY || !process.env.MEMWAL_ACCOUNT_ID)) { + return Response.json( + { error: "[MemWal] No accountId configured — sign in with Enoki or set MEMWAL_ACCOUNT_ID in .env" }, + { status: 401 }, + ); + } + + const result = await rememberText(text, key, accountId); + return Response.json(result); + } catch (error) { + console.error("[memory/remember-one] Error:", error); + return Response.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 }, + ); + } } diff --git a/apps/noter/package/feature/note/hook/use-note-memory-save.ts b/apps/noter/package/feature/note/hook/use-note-memory-save.ts index 4a414260..0c106145 100644 --- a/apps/noter/package/feature/note/hook/use-note-memory-save.ts +++ b/apps/noter/package/feature/note/hook/use-note-memory-save.ts @@ -13,8 +13,20 @@ import { useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { $getMemoryById } from "../plugins/MemoryHighlightPlugin"; -import { UPDATE_MEMORY_STATUS_COMMAND } from "../plugins/MemoryHighlightPlugin"; +import { $getMemoryById, UPDATE_MEMORY_STATUS_COMMAND } from "../plugins/MemoryHighlightPlugin"; +import { STORAGE_KEYS } from "@/feature/auth/constant"; +import type { SessionData } from "@/feature/auth/domain/type"; + +function getSessionId(): string | null { + if (typeof window === "undefined") return null; + try { + const raw = sessionStorage.getItem(STORAGE_KEYS.sessionId); + if (!raw) return null; + return (JSON.parse(raw) as SessionData | null)?.sessionId ?? null; + } catch { + return null; + } +} export function useNoteMemorySave() { const [editor] = useLexicalComposerContext(); @@ -47,10 +59,13 @@ export function useNoteMemorySave() { status: "uploading", }); - // Call server-side API to remember (v2 SDK) + const sessionId = getSessionId(); const response = await fetch("/api/memory/remember-one", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...(sessionId ? { "x-session-id": sessionId } : {}), + }, body: JSON.stringify({ text: memoryText }), });