From b92f6384a451d2b57fd3225356a663adb758a1e0 Mon Sep 17 00:00:00 2001 From: sepehr Date: Tue, 28 Apr 2026 22:18:46 +0200 Subject: [PATCH] fix: chat memory lost between messages + per-note history Chat (AIChat floating widget): conversationId was never captured from the API response, so every message created a new conversation with no context. Now creates the conversation upfront before streaming (same pattern as ChatContainer) so the ID persists across messages. Note history: was stored globally in UserAISettings, so enabling history on one note enabled it for ALL notes. Now each Note has its own historyEnabled boolean field. The "Enable history" action only affects the specific note. A migration adds the column with default false. Co-Authored-By: Claude Opus 4.7 --- memento-note/app/actions/notes.ts | 37 ++++++++++++------- memento-note/app/api/notes/[id]/route.ts | 2 +- memento-note/components/ai-chat.tsx | 17 ++++++++- .../components/chat/chat-container.tsx | 14 +++---- memento-note/components/home-client.tsx | 18 ++++----- .../components/note-inline-editor.tsx | 16 +++++--- .../components/notes-main-section.tsx | 6 +-- memento-note/components/notes-tabs-view.tsx | 20 ++++------ memento-note/lib/types.ts | 1 + .../migration.sql | 2 + memento-note/prisma/schema.prisma | 1 + 11 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 memento-note/prisma/migrations/20260429010000_add_note_history_per_note/migration.sql diff --git a/memento-note/app/actions/notes.ts b/memento-note/app/actions/notes.ts index 560897d..f7f2825 100644 --- a/memento-note/app/actions/notes.ts +++ b/memento-note/app/actions/notes.ts @@ -436,14 +436,12 @@ export async function commitNoteHistory(noteId: string) { const session = await auth() if (!session?.user?.id) throw new Error('Unauthorized') - const enabled = await isNoteHistoryEnabledForUser(session.user.id) - if (!enabled) throw new Error('History is disabled') - const note = await prisma.note.findFirst({ where: { id: noteId, userId: session.user.id }, - select: { id: true }, + select: { id: true, historyEnabled: true }, }) if (!note) throw new Error('Note not found') + if (!note.historyEnabled) throw new Error('History is disabled for this note') await createNoteHistorySnapshot({ noteId: note.id, @@ -466,6 +464,22 @@ export async function deleteNoteHistoryEntry(noteId: string, historyEntryId: str }) } +export async function enableNoteHistory(noteId: string) { + const session = await auth() + if (!session?.user?.id) throw new Error('Unauthorized') + + const note = await prisma.note.findFirst({ + where: { id: noteId, userId: session.user.id }, + select: { id: true }, + }) + if (!note) throw new Error('Note not found') + + await prisma.note.update({ + where: { id: noteId }, + data: { historyEnabled: true }, + }) +} + // Search notes - DB-side filtering (fast) with optional semantic search // Supports contextual search within notebook (IA5) export async function searchNotes(query: string, useSemantic: boolean = false, notebookId?: string) { @@ -641,14 +655,9 @@ export async function createNote(data: { } try { - const historyEnabled = await isNoteHistoryEnabledForUser(session.user.id) - if (historyEnabled) { - await createNoteHistorySnapshot({ - noteId: note.id, - userId: session.user.id, - reason: 'create', - }) - } + // New notes start with historyEnabled=false (schema default), + // so no initial snapshot is needed here. + // History is enabled per-note via enableNoteHistory() action. } catch (snapshotError) { console.error('[HISTORY] Failed to create initial snapshot:', snapshotError) } @@ -783,7 +792,7 @@ export async function updateNote(id: string, data: { try { const oldNote = await prisma.note.findUnique({ where: { id, userId: session.user.id }, - select: { labels: true, notebookId: true, reminder: true, content: true, title: true } + select: { labels: true, notebookId: true, reminder: true, content: true, title: true, historyEnabled: true } }) const oldLabels: string[] = oldNote?.labels ? JSON.parse(oldNote.labels) : [] const oldNotebookId = oldNote?.notebookId @@ -854,7 +863,7 @@ export async function updateNote(id: string, data: { } try { - const historyEnabled = await isNoteHistoryEnabledForUser(session.user.id) + const historyEnabled = oldNote?.historyEnabled === true if (historyEnabled && shouldCaptureHistorySnapshot(data as Record)) { const mode = await getNoteHistoryMode(session.user.id) if (mode === 'manual') { diff --git a/memento-note/app/api/notes/[id]/route.ts b/memento-note/app/api/notes/[id]/route.ts index 4e43577..de0c05d 100644 --- a/memento-note/app/api/notes/[id]/route.ts +++ b/memento-note/app/api/notes/[id]/route.ts @@ -142,7 +142,7 @@ export async function PUT( }) try { - const historyEnabled = await isNoteHistoryEnabledForUser(session.user.id) + const historyEnabled = existingNote.historyEnabled === true if (historyEnabled && shouldCaptureHistorySnapshot(updateData)) { const mode = await getNoteHistoryMode(session.user.id) if (mode === 'auto') { diff --git a/memento-note/components/ai-chat.tsx b/memento-note/components/ai-chat.tsx index 63732c2..39b0d66 100644 --- a/memento-note/components/ai-chat.tsx +++ b/memento-note/components/ai-chat.tsx @@ -13,6 +13,7 @@ import { useWebSearchAvailable } from '@/hooks/use-web-search-available' import { useNotebooks } from '@/context/notebooks-context' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { toast } from 'sonner' +import { createConversation } from '@/app/actions/chat-actions' function getTextContent(msg: UIMessage): string { if (msg.parts && Array.isArray(msg.parts)) { @@ -68,6 +69,20 @@ export function AIChat() { const text = input.trim() if (!text || isLoading) return setInput('') + + // Create conversation upfront so we have the ID for continuity + let convId = conversationId + if (!convId) { + try { + const result = await createConversation(text, chatScope !== 'all' ? chatScope : undefined) + convId = result.id + setConversationId(convId) + } catch { + toast.error(t('chat.createError')) + return + } + } + await sendMessage( { text }, { @@ -76,7 +91,7 @@ export function AIChat() { chatScope, notebookId: chatScope !== 'all' ? chatScope : undefined, webSearch: webSearch && webSearchAvailable, - conversationId, + conversationId: convId, language, } } diff --git a/memento-note/components/chat/chat-container.tsx b/memento-note/components/chat/chat-container.tsx index 9cc98c6..fac1e45 100644 --- a/memento-note/components/chat/chat-container.tsx +++ b/memento-note/components/chat/chat-container.tsx @@ -50,6 +50,13 @@ export function ChatContainer({ initialConversations, notebooks, webSearchAvaila const isLoading = status === 'submitted' || status === 'streaming' + const refreshConversations = useCallback(async () => { + try { + const updated = await getConversations() + setConversations(updated) + } catch {} + }, []) + // Timeout warning: show toast if response takes > 30s useEffect(() => { if (!isLoading) return @@ -105,13 +112,6 @@ export function ChatContainer({ initialConversations, notebooks, webSearchAvaila // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentId]) - const refreshConversations = useCallback(async () => { - try { - const updated = await getConversations() - setConversations(updated) - } catch {} - }, []) - const handleSendMessage = async (content: string, notebookId?: string) => { if (notebookId) { setSelectedNotebook(notebookId) diff --git a/memento-note/components/home-client.tsx b/memento-note/components/home-client.tsx index 982c494..55ed711 100644 --- a/memento-note/components/home-client.tsx +++ b/memento-note/components/home-client.tsx @@ -4,8 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import dynamic from 'next/dynamic' import { Note } from '@/lib/types' -import { updateAISettings } from '@/app/actions/ai-settings' -import { getAllNotes, searchNotes } from '@/app/actions/notes' +import { getAllNotes, searchNotes, enableNoteHistory } from '@/app/actions/notes' import { NoteInput } from '@/components/note-input' import { NotesMainSection, type NotesViewMode } from '@/components/notes-main-section' import { NotesViewToggle } from '@/components/notes-view-toggle' @@ -62,7 +61,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { initialNotes.filter(n => n.isPinned) ) const [notesViewMode, setNotesViewMode] = useState(initialSettings.notesViewMode) - const [noteHistoryEnabled, setNoteHistoryEnabled] = useState(initialSettings.noteHistory) const [noteHistoryMode] = useState<'manual' | 'auto'>(initialSettings.noteHistoryMode) const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null) const [isLoading, setIsLoading] = useState(false) // false by default — data is pre-loaded @@ -145,9 +143,12 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { setHistoryOpen(true) }, []) - const handleEnableHistory = useCallback(async () => { - await updateAISettings({ noteHistory: true }) - setNoteHistoryEnabled(true) + const handleEnableHistory = useCallback(async (noteId: string) => { + await enableNoteHistory(noteId) + // Update the specific note in state + setNotes((prev) => prev.map((n) => (n.id === noteId ? { ...n, historyEnabled: true } : n))) + setPinnedNotes((prev) => prev.map((n) => (n.id === noteId ? { ...n, historyEnabled: true } : n))) + setEditingNote((prev) => (prev?.note.id === noteId ? { ...prev, note: { ...prev.note, historyEnabled: true } } : prev)) }, []) const handleHistoryRestored = useCallback((restored: Note) => { @@ -411,7 +412,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { onEdit={(note, readOnly) => setEditingNote({ note, readOnly })} onSizeChange={handleSizeChange} currentNotebookId={searchParams.get('notebook')} - noteHistoryEnabled={noteHistoryEnabled} noteHistoryMode={noteHistoryMode} onOpenHistory={handleOpenHistory} onEnableHistory={handleEnableHistory} @@ -469,8 +469,8 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { open={historyOpen} onOpenChange={setHistoryOpen} note={historyNote} - enabled={noteHistoryEnabled} - onEnableHistory={handleEnableHistory} + enabled={!!historyNote?.historyEnabled} + onEnableHistory={async () => { if (historyNote) await handleEnableHistory(historyNote.id) }} onRestored={handleHistoryRestored} /> diff --git a/memento-note/components/note-inline-editor.tsx b/memento-note/components/note-inline-editor.tsx index 75de3f6..2470aed 100644 --- a/memento-note/components/note-inline-editor.tsx +++ b/memento-note/components/note-inline-editor.tsx @@ -67,7 +67,7 @@ interface NoteInlineEditorProps { onArchive?: (noteId: string) => void onChange?: (noteId: string, fields: Partial) => void onOpenHistory?: (note: Note) => void - noteHistoryEnabled?: boolean + onEnableHistory?: (noteId: string) => Promise noteHistoryMode?: 'manual' | 'auto' colorKey: NoteColor /** If true and the note is a Markdown note, open directly in preview mode */ @@ -98,7 +98,7 @@ export function NoteInlineEditor({ onArchive, onChange, onOpenHistory, - noteHistoryEnabled = false, + onEnableHistory, noteHistoryMode = 'manual', colorKey, defaultPreviewMode = false, @@ -532,7 +532,7 @@ export function NoteInlineEditor({ {/* Right group: meta actions + save indicator */}
- {noteHistoryEnabled && noteHistoryMode === 'manual' && ( + {note.historyEnabled && noteHistoryMode === 'manual' && (