fix(memory-echo): fix broken AI provider config and auto-generate missing embeddings

- Fix critical bug: used prisma.systemConfig.findFirst() which returned
  a single {key,value} record instead of the full config object needed
  by getAIProvider(). Replaced with getSystemConfig() that returns all
  config as a proper Record<string,string>.
- Add ensureEmbeddings() to auto-generate embeddings for notes that
  lack them before searching for connections. This fixes the case where
  notes created without an AI provider configured never got embeddings,
  making Memory Echo silently return zero connections.
- Restore demo mode polling (15s interval after dismiss) in the
  notification component.
- Integrate ComparisonModal and FusionModal in the notification card
  with merge button for direct note fusion from the notification.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-19 22:05:19 +02:00
parent 25529a24b8
commit 389f85937a
2 changed files with 232 additions and 222 deletions

View File

@@ -1,5 +1,6 @@
import { getAIProvider } from '../factory'
import { cosineSimilarity } from '@/lib/utils'
import { getSystemConfig } from '@/lib/config'
import prisma from '@/lib/prisma'
export interface NoteConnection {
@@ -52,10 +53,53 @@ export class MemoryEchoService {
private readonly MIN_DAYS_APART_DEMO = 0 // No delay for demo mode
private readonly MAX_INSIGHTS_PER_USER = 100 // Prevent spam
/**
* Generate embeddings for notes that don't have one yet
*/
private async ensureEmbeddings(userId: string): Promise<void> {
const notesWithoutEmbeddings = await prisma.note.findMany({
where: {
userId,
isArchived: false,
trashedAt: null,
noteEmbedding: { is: null }
},
select: { id: true, content: true }
})
if (notesWithoutEmbeddings.length === 0) return
try {
const config = await getSystemConfig()
const provider = getAIProvider(config)
for (const note of notesWithoutEmbeddings) {
if (!note.content || note.content.trim().length === 0) continue
try {
const embedding = await provider.getEmbeddings(note.content)
if (embedding && embedding.length > 0) {
await prisma.noteEmbedding.upsert({
where: { noteId: note.id },
create: { noteId: note.id, embedding: JSON.stringify(embedding) },
update: { embedding: JSON.stringify(embedding) }
})
}
} catch {
// Skip this note, continue with others
}
}
} catch {
// Provider not configured — nothing we can do
}
}
/**
* Find meaningful connections between user's notes
*/
async findConnections(userId: string, demoMode: boolean = false): Promise<NoteConnection[]> {
// Ensure all notes have embeddings before searching for connections
await this.ensureEmbeddings(userId)
// Get all user's notes with embeddings
const notes = await prisma.note.findMany({
where: {
@@ -151,8 +195,8 @@ export class MemoryEchoService {
note2Content: string
): Promise<string> {
try {
const config = await prisma.systemConfig.findFirst()
const provider = getAIProvider(config || undefined)
const config = await getSystemConfig()
const provider = getAIProvider(config)
const note1Desc = note1Title || 'Untitled note'
const note2Desc = note2Title || 'Untitled note'
@@ -366,6 +410,9 @@ Explain in one brief sentence (max 15 words) why these notes are connected. Focu
* Get all connections for a specific note
*/
async getConnectionsForNote(noteId: string, userId: string): Promise<NoteConnection[]> {
// Ensure all notes have embeddings before searching
await this.ensureEmbeddings(userId)
// Get the note with embedding
const targetNote = await prisma.note.findUnique({
where: { id: noteId },