Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/chatbot/components/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type Dispatch,
memo,
type ReactNode,
type RefObject,
type SetStateAction,
useEffect,
useRef,
Expand Down Expand Up @@ -319,12 +320,12 @@ const PureToolbar = ({
artifactKind: ArtifactKind;
}) => {
const toolbarRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const [selectedTool, setSelectedTool] = useState<string | null>(null);
const [isAnimating, setIsAnimating] = useState(false);

useOnClickOutside(toolbarRef as React.RefObject<HTMLElement>, () => {
useOnClickOutside(toolbarRef as RefObject<HTMLElement>, () => {
setIsToolbarVisible(false);
setSelectedTool(null);
});
Expand Down
2 changes: 1 addition & 1 deletion apps/chatbot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"lucide-react": "^0.446.0",
"motion": "^12.23.26",
"nanoid": "^5.1.3",
"next": "16.0.10",
"next": "16.2.6",
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.3.0",
"orderedmap": "^2.1.1",
Expand Down
2,667 changes: 1,896 additions & 771 deletions apps/chatbot/pnpm-lock.yaml

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions apps/noter/app/api/memory/remember-one/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* 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";
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 { 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 },
);
}
}
4 changes: 2 additions & 2 deletions apps/noter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"jwt-decode": "^4.0.0",
"lexical": "^0.41.0",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"next": "16.2.6",
"next-themes": "^0.4.6",
"postgres": "^3.4.8",
"prismjs": "^1.30.0",
Expand Down Expand Up @@ -96,4 +96,4 @@
"@types/react": "^19",
"@types/react-dom": "^19"
}
}
}
25 changes: 20 additions & 5 deletions apps/noter/package/feature/note/hook/use-note-memory-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -47,10 +59,13 @@ export function useNoteMemorySave() {
status: "uploading",
});

// Call server-side API to remember (v2 SDK)
const response = await fetch("/api/memory/remember", {
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 }),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
22 changes: 12 additions & 10 deletions apps/noter/package/feature/note/ui/memory-hover-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -205,16 +206,17 @@ export function MemoryHoverPreview({
<span className="text-xs text-muted-foreground font-mono truncate">
{memory.memwalMemoryId.slice(0, 16)}...
</span>
<button
onClick={(e) => {
e.stopPropagation();
// TODO: Open in Sui Explorer
navigator.clipboard.writeText(memory.memwalMemoryId!);
}}
className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
<ExternalLink className="h-3 w-3" />
</button>
{memory.memwalBlobId && (
<a
href={`https://walruscan.com/${ZKLOGIN_CONFIG.network}/blob/${memory.memwalBlobId}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
<ExternalLink className="h-3 w-3" />
</a>
)}
</div>

{/* Blob ID */}
Expand Down
24 changes: 12 additions & 12 deletions apps/noter/package/feature/note/ui/memory-panel-enhanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -433,18 +434,17 @@ const MemoryCard = forwardRef<HTMLDivElement, MemoryCardProps>(
<div className="space-y-1 text-xs text-muted-foreground font-mono">
<div className="flex items-center justify-between gap-2">
<span className="truncate">Memory: {memory.memwalMemoryId.slice(0, 12)}...</span>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={(e) => {
e.stopPropagation();
// TODO: Add link to Sui Explorer
toast.info("Opening in Sui Explorer...");
}}
>
<ExternalLink className="h-3 w-3" />
</Button>
{memory.memwalBlobId && (
<a
href={`https://walruscan.com/${ZKLOGIN_CONFIG.network}/blob/${memory.memwalBlobId}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="shrink-0 p-0.5 text-blue-600 dark:text-blue-400 hover:underline"
>
<ExternalLink className="h-3 w-3" />
</a>
)}
</div>
{memory.memwalBlobId && (
<div className="truncate">Blob: {memory.memwalBlobId.slice(0, 12)}...</div>
Expand Down
9 changes: 8 additions & 1 deletion apps/noter/package/feature/note/ui/note-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import { MarketingBorder } from "@/package/shared/components/border";
import { Button } from "@/shared/components/ui/button";
import { Check, Loader2, Save, XCircle } 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
Expand All @@ -56,6 +60,7 @@ const editorConfig = {
TableCellNode,
TableRowNode,
HorizontalRuleNode,
MemoryHighlightNode,
],
onError(error: Error) {
console.error("Lexical error:", error);
Expand Down Expand Up @@ -249,6 +254,7 @@ export function NoteEditor({ noteId }: NoteEditorProps) {
{/* Toolbar */}
<div className="border-b p-4 flex items-center justify-start gap-3">
<SaveNoteButton onSaveToDb={saveImmediate} />
<MemoryDetectButton noteId={noteId} />
</div>

{/* Editor */}
Expand All @@ -275,10 +281,11 @@ export function NoteEditor({ noteId }: NoteEditorProps) {
<CodeHighlightPlugin />
<MarkdownShortcutPlugin transformers={CHAT_TRANSFORMERS} />
<AutoSavePlugin onSave={save} />

<MemoryHighlightPlugin />

<MarketingBorder />
</div>
<MemoryPanelEnhanced onSaveComplete={saveImmediate} />
</div>
</LexicalComposer>
);
Expand Down
Loading
Loading