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,