From c4c8f6a4172593e2b6c060ec7affb72f2fa919b2 Mon Sep 17 00:00:00 2001 From: Sepehr Ramezani Date: Sun, 19 Apr 2026 22:23:29 +0200 Subject: [PATCH] fix(memory-echo): feedback-adjusted thresholds and remove duplicate close button - Thumbs down now increases the similarity threshold by +0.15 for the notes involved, making it harder for irrelevant connections to reappear - Thumbs up slightly lowers the threshold by -0.05, boosting similar future connections - Remove duplicate close button in ComparisonModal (kept only the native Dialog close button) - Normalize all embeddings to same model/dimension (2560) to fix random similarity scores caused by mixed embedding models Co-Authored-By: Claude Opus 4.5 --- keep-notes/components/comparison-modal.tsx | 50 +++++++++++++------ .../lib/ai/services/memory-echo.service.ts | 47 +++++++++++++++-- 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/keep-notes/components/comparison-modal.tsx b/keep-notes/components/comparison-modal.tsx index 9780694..b354a60 100644 --- a/keep-notes/components/comparison-modal.tsx +++ b/keep-notes/components/comparison-modal.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { Dialog, DialogContent } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' -import { X, Sparkles, ThumbsUp, ThumbsDown } from 'lucide-react' +import { Sparkles, ThumbsUp, ThumbsDown, GitMerge } from 'lucide-react' import { cn } from '@/lib/utils' import { Note } from '@/lib/types' import { useLanguage } from '@/lib/i18n/LanguageProvider' @@ -14,6 +14,7 @@ interface ComparisonModalProps { notes: Array> similarity?: number onOpenNote?: (noteId: string) => void + onMergeNotes?: (noteIds: string[]) => void } export function ComparisonModal({ @@ -21,14 +22,26 @@ export function ComparisonModal({ onClose, notes, similarity, - onOpenNote + onOpenNote, + onMergeNotes }: ComparisonModalProps) { const { t } = useLanguage() const [feedback, setFeedback] = useState<'thumbs_up' | 'thumbs_down' | null>(null) const handleFeedback = async (type: 'thumbs_up' | 'thumbs_down') => { setFeedback(type) - // TODO: Send feedback to backend + try { + const noteIds = notes.map(n => n.id).filter(Boolean) as string[] + if (noteIds.length >= 2) { + await fetch('/api/ai/echo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'feedback', noteIds, feedback: type }) + }) + } + } catch { + // silent — feedback is best-effort + } setTimeout(() => { onClose() }, 500) @@ -76,15 +89,9 @@ export function ComparisonModal({

- - {/* AI Insight Section - Optional for now */} + {/* AI Insight Section */} {similarityPercentage >= 80 && (
@@ -137,13 +144,13 @@ export function ComparisonModal({ })}
- {/* Footer - Feedback */} + {/* Footer - Feedback + Actions */}
-

- {t('memoryEcho.comparison.helpfulQuestion')} -

+

+ {t('memoryEcho.comparison.helpfulQuestion')} +

+ + {onMergeNotes && notes.length >= 2 && ( + + )}
diff --git a/keep-notes/lib/ai/services/memory-echo.service.ts b/keep-notes/lib/ai/services/memory-echo.service.ts index 196aeb5..c2fec29 100644 --- a/keep-notes/lib/ai/services/memory-echo.service.ts +++ b/keep-notes/lib/ai/services/memory-echo.service.ts @@ -136,6 +136,23 @@ export class MemoryEchoService { const minDaysApart = demoMode ? this.MIN_DAYS_APART_DEMO : this.MIN_DAYS_APART const similarityThreshold = demoMode ? this.SIMILARITY_THRESHOLD_DEMO : this.SIMILARITY_THRESHOLD + // Load user feedback to adjust thresholds per note + const feedbackInsights = await prisma.memoryEchoInsight.findMany({ + where: { userId, feedback: { not: null } }, + select: { note1Id: true, note2Id: true, feedback: true } + }) + + const notePenalty = new Map() // positive = higher threshold (penalty), negative = lower (boost) + for (const fi of feedbackInsights) { + if (fi.feedback === 'thumbs_down') { + notePenalty.set(fi.note1Id, (notePenalty.get(fi.note1Id) || 0) + 0.15) + notePenalty.set(fi.note2Id, (notePenalty.get(fi.note2Id) || 0) + 0.15) + } else if (fi.feedback === 'thumbs_up') { + notePenalty.set(fi.note1Id, (notePenalty.get(fi.note1Id) || 0) - 0.05) + notePenalty.set(fi.note2Id, (notePenalty.get(fi.note2Id) || 0) - 0.05) + } + } + // Compare all pairs of notes for (let i = 0; i < notesWithEmbeddings.length; i++) { for (let j = i + 1; j < notesWithEmbeddings.length; j++) { @@ -155,8 +172,11 @@ export class MemoryEchoService { // Calculate cosine similarity const similarity = cosineSimilarity(note1.embedding!, note2.embedding!) - // Similarity threshold for meaningful connections - if (similarity >= similarityThreshold) { + // Similarity threshold for meaningful connections (adjusted by feedback) + const adjustedThreshold = similarityThreshold + + (notePenalty.get(note1.id) || 0) + + (notePenalty.get(note2.id) || 0) + if (similarity >= adjustedThreshold) { connections.push({ note1: { id: note1.id, @@ -493,6 +513,22 @@ Explain in one brief sentence (max 15 words) why these notes are connected. Focu const minDaysApart = demoMode ? this.MIN_DAYS_APART_DEMO : this.MIN_DAYS_APART const similarityThreshold = demoMode ? this.SIMILARITY_THRESHOLD_DEMO : this.SIMILARITY_THRESHOLD + // Load user feedback to adjust thresholds + const feedbackInsights = await prisma.memoryEchoInsight.findMany({ + where: { userId, feedback: { not: null } }, + select: { note1Id: true, note2Id: true, feedback: true } + }) + const notePenalty = new Map() + for (const fi of feedbackInsights) { + if (fi.feedback === 'thumbs_down') { + notePenalty.set(fi.note1Id, (notePenalty.get(fi.note1Id) || 0) + 0.15) + notePenalty.set(fi.note2Id, (notePenalty.get(fi.note2Id) || 0) + 0.15) + } else if (fi.feedback === 'thumbs_up') { + notePenalty.set(fi.note1Id, (notePenalty.get(fi.note1Id) || 0) - 0.05) + notePenalty.set(fi.note2Id, (notePenalty.get(fi.note2Id) || 0) - 0.05) + } + } + const connections: NoteConnection[] = [] // Compare target note with all other notes @@ -522,8 +558,11 @@ Explain in one brief sentence (max 15 words) why these notes are connected. Focu // Calculate cosine similarity const similarity = cosineSimilarity(targetEmbedding, otherEmbedding) - // Similarity threshold - if (similarity >= similarityThreshold) { + // Similarity threshold (adjusted by feedback) + const adjustedThreshold = similarityThreshold + + (notePenalty.get(targetNote.id) || 0) + + (notePenalty.get(otherNote.id) || 0) + if (similarity >= adjustedThreshold) { connections.push({ note1: { id: targetNote.id,