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 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-19 22:23:29 +02:00
parent 389f85937a
commit c4c8f6a417
2 changed files with 79 additions and 18 deletions

View File

@@ -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<Partial<Note>>
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({
</p>
</div>
</div>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
<X className="h-6 w-6" />
</button>
</div>
{/* AI Insight Section - Optional for now */}
{/* AI Insight Section */}
{similarityPercentage >= 80 && (
<div className="px-6 py-4 bg-amber-50 dark:bg-amber-950/20 border-b dark:border-zinc-700">
<div className="flex items-start gap-2">
@@ -137,13 +144,13 @@ export function ComparisonModal({
})}
</div>
{/* Footer - Feedback */}
{/* Footer - Feedback + Actions */}
<div className="px-6 py-4 border-t dark:border-zinc-700 bg-gray-50 dark:bg-zinc-800/50">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('memoryEcho.comparison.helpfulQuestion')}
</p>
<div className="flex items-center gap-2">
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('memoryEcho.comparison.helpfulQuestion')}
</p>
<Button
size="sm"
variant={feedback === 'thumbs_up' ? 'default' : 'outline'}
@@ -167,6 +174,21 @@ export function ComparisonModal({
{t('memoryEcho.comparison.notHelpful')}
</Button>
</div>
{onMergeNotes && notes.length >= 2 && (
<Button
size="sm"
className="bg-purple-600 hover:bg-purple-700 text-white"
onClick={() => {
const noteIds = notes.map(n => n.id).filter(Boolean) as string[]
onMergeNotes(noteIds)
onClose()
}}
>
<GitMerge className="h-4 w-4 mr-2" />
{t('memoryEcho.editorSection.mergeAll')}
</Button>
)}
</div>
</div>
</DialogContent>

View File

@@ -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<string, number>() // 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<string, number>()
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,