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:
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
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 { cn } from '@/lib/utils'
|
||||||
import { Note } from '@/lib/types'
|
import { Note } from '@/lib/types'
|
||||||
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
import { useLanguage } from '@/lib/i18n/LanguageProvider'
|
||||||
@@ -14,6 +14,7 @@ interface ComparisonModalProps {
|
|||||||
notes: Array<Partial<Note>>
|
notes: Array<Partial<Note>>
|
||||||
similarity?: number
|
similarity?: number
|
||||||
onOpenNote?: (noteId: string) => void
|
onOpenNote?: (noteId: string) => void
|
||||||
|
onMergeNotes?: (noteIds: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComparisonModal({
|
export function ComparisonModal({
|
||||||
@@ -21,14 +22,26 @@ export function ComparisonModal({
|
|||||||
onClose,
|
onClose,
|
||||||
notes,
|
notes,
|
||||||
similarity,
|
similarity,
|
||||||
onOpenNote
|
onOpenNote,
|
||||||
|
onMergeNotes
|
||||||
}: ComparisonModalProps) {
|
}: ComparisonModalProps) {
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const [feedback, setFeedback] = useState<'thumbs_up' | 'thumbs_down' | null>(null)
|
const [feedback, setFeedback] = useState<'thumbs_up' | 'thumbs_down' | null>(null)
|
||||||
|
|
||||||
const handleFeedback = async (type: 'thumbs_up' | 'thumbs_down') => {
|
const handleFeedback = async (type: 'thumbs_up' | 'thumbs_down') => {
|
||||||
setFeedback(type)
|
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(() => {
|
setTimeout(() => {
|
||||||
onClose()
|
onClose()
|
||||||
}, 500)
|
}, 500)
|
||||||
@@ -76,15 +89,9 @@ export function ComparisonModal({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* AI Insight Section - Optional for now */}
|
{/* AI Insight Section */}
|
||||||
{similarityPercentage >= 80 && (
|
{similarityPercentage >= 80 && (
|
||||||
<div className="px-6 py-4 bg-amber-50 dark:bg-amber-950/20 border-b dark:border-zinc-700">
|
<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">
|
<div className="flex items-start gap-2">
|
||||||
@@ -137,13 +144,13 @@ export function ComparisonModal({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</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="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">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{t('memoryEcho.comparison.helpfulQuestion')}
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={feedback === 'thumbs_up' ? 'default' : 'outline'}
|
variant={feedback === 'thumbs_up' ? 'default' : 'outline'}
|
||||||
@@ -167,6 +174,21 @@ export function ComparisonModal({
|
|||||||
{t('memoryEcho.comparison.notHelpful')}
|
{t('memoryEcho.comparison.notHelpful')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -136,6 +136,23 @@ export class MemoryEchoService {
|
|||||||
const minDaysApart = demoMode ? this.MIN_DAYS_APART_DEMO : this.MIN_DAYS_APART
|
const minDaysApart = demoMode ? this.MIN_DAYS_APART_DEMO : this.MIN_DAYS_APART
|
||||||
const similarityThreshold = demoMode ? this.SIMILARITY_THRESHOLD_DEMO : this.SIMILARITY_THRESHOLD
|
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
|
// Compare all pairs of notes
|
||||||
for (let i = 0; i < notesWithEmbeddings.length; i++) {
|
for (let i = 0; i < notesWithEmbeddings.length; i++) {
|
||||||
for (let j = i + 1; j < notesWithEmbeddings.length; j++) {
|
for (let j = i + 1; j < notesWithEmbeddings.length; j++) {
|
||||||
@@ -155,8 +172,11 @@ export class MemoryEchoService {
|
|||||||
// Calculate cosine similarity
|
// Calculate cosine similarity
|
||||||
const similarity = cosineSimilarity(note1.embedding!, note2.embedding!)
|
const similarity = cosineSimilarity(note1.embedding!, note2.embedding!)
|
||||||
|
|
||||||
// Similarity threshold for meaningful connections
|
// Similarity threshold for meaningful connections (adjusted by feedback)
|
||||||
if (similarity >= similarityThreshold) {
|
const adjustedThreshold = similarityThreshold
|
||||||
|
+ (notePenalty.get(note1.id) || 0)
|
||||||
|
+ (notePenalty.get(note2.id) || 0)
|
||||||
|
if (similarity >= adjustedThreshold) {
|
||||||
connections.push({
|
connections.push({
|
||||||
note1: {
|
note1: {
|
||||||
id: note1.id,
|
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 minDaysApart = demoMode ? this.MIN_DAYS_APART_DEMO : this.MIN_DAYS_APART
|
||||||
const similarityThreshold = demoMode ? this.SIMILARITY_THRESHOLD_DEMO : this.SIMILARITY_THRESHOLD
|
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[] = []
|
const connections: NoteConnection[] = []
|
||||||
|
|
||||||
// Compare target note with all other notes
|
// 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
|
// Calculate cosine similarity
|
||||||
const similarity = cosineSimilarity(targetEmbedding, otherEmbedding)
|
const similarity = cosineSimilarity(targetEmbedding, otherEmbedding)
|
||||||
|
|
||||||
// Similarity threshold
|
// Similarity threshold (adjusted by feedback)
|
||||||
if (similarity >= similarityThreshold) {
|
const adjustedThreshold = similarityThreshold
|
||||||
|
+ (notePenalty.get(targetNote.id) || 0)
|
||||||
|
+ (notePenalty.get(otherNote.id) || 0)
|
||||||
|
if (similarity >= adjustedThreshold) {
|
||||||
connections.push({
|
connections.push({
|
||||||
note1: {
|
note1: {
|
||||||
id: targetNote.id,
|
id: targetNote.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user