chore: snapshot before performance optimization

This commit is contained in:
Sepehr Ramezani
2026-04-17 21:14:43 +02:00
parent b6a548acd8
commit 2eceb32fd4
95 changed files with 4357 additions and 1942 deletions

View File

@@ -454,7 +454,7 @@ Deine Antwort (nur JSON):
let names: string[] = []
if (note.labels) {
try {
const parsed = JSON.parse(note.labels) as unknown
const parsed = note.labels as unknown
names = Array.isArray(parsed)
? parsed.filter((n): n is string => typeof n === 'string' && n.trim().length > 0)
: []
@@ -471,7 +471,7 @@ Deine Antwort (nur JSON):
await prisma.note.update({
where: { id: noteId },
data: {
labels: JSON.stringify(names),
labels: names as any,
labelRelations: {
connect: { id: label.id },
},

View File

@@ -175,26 +175,17 @@ export class EmbeddingService {
}
/**
* Serialize embedding to JSON-safe format (for storage)
* Pass-through — embeddings are stored as native JSONB in PostgreSQL
*/
serialize(embedding: number[]): string {
return JSON.stringify(embedding)
serialize(embedding: number[]): number[] {
return embedding
}
/**
* Deserialize embedding from JSON string
* Pass-through — embeddings come back already parsed from PostgreSQL
*/
deserialize(jsonString: string): number[] {
try {
const parsed = JSON.parse(jsonString)
if (!Array.isArray(parsed)) {
throw new Error('Invalid embedding format')
}
return parsed
} catch (error) {
console.error('Error deserializing embedding:', error)
throw new Error('Failed to deserialize embedding')
}
deserialize(embedding: number[]): number[] {
return embedding
}
/**

View File

@@ -77,11 +77,11 @@ export class MemoryEchoService {
return [] // Need at least 2 notes to find connections
}
// Parse embeddings
// Parse embeddings (already native Json from PostgreSQL)
const notesWithEmbeddings = notes
.map(note => ({
...note,
embedding: note.embedding ? JSON.parse(note.embedding) : null
embedding: note.embedding ? JSON.parse(note.embedding) as number[] : null
}))
.filter(note => note.embedding && Array.isArray(note.embedding))
@@ -108,7 +108,7 @@ export class MemoryEchoService {
}
// Calculate cosine similarity
const similarity = cosineSimilarity(note1.embedding, note2.embedding)
const similarity = cosineSimilarity(note1.embedding!, note2.embedding!)
// Similarity threshold for meaningful connections
if (similarity >= similarityThreshold) {
@@ -348,9 +348,9 @@ Explain in one brief sentence (max 15 words) why these notes are connected. Focu
feedbackType: feedback,
feature: 'memory_echo',
originalContent: JSON.stringify({ insightId }),
metadata: JSON.stringify({
metadata: {
timestamp: new Date().toISOString()
})
} as any
}
})
}
@@ -426,8 +426,9 @@ Explain in one brief sentence (max 15 words) why these notes are connected. Focu
return []
}
// Parse target note embedding
const targetEmbedding = JSON.parse(targetNote.embedding)
// Target note embedding (already native Json from PostgreSQL)
const targetEmbedding = targetNote.embedding ? JSON.parse(targetNote.embedding) as number[] : null
if (!targetEmbedding) return []
// Check if user has demo mode enabled
const settings = await prisma.userAISettings.findUnique({
@@ -444,7 +445,8 @@ Explain in one brief sentence (max 15 words) why these notes are connected. Focu
for (const otherNote of otherNotes) {
if (!otherNote.embedding) continue
const otherEmbedding = JSON.parse(otherNote.embedding)
const otherEmbedding = otherNote.embedding ? JSON.parse(otherNote.embedding) as number[] : null
if (!otherEmbedding) continue
// Check if this connection was dismissed
const pairKey1 = `${targetNote.id}-${otherNote.id}`

View File

@@ -192,7 +192,7 @@ export class SemanticSearchService {
// Calculate similarities for all notes
const similarities = notes.map(note => {
const noteEmbedding = embeddingService.deserialize(note.embedding || '[]')
const noteEmbedding = note.embedding ? JSON.parse(note.embedding) as number[] : []
const similarity = embeddingService.calculateCosineSimilarity(
queryEmbedding,
noteEmbedding
@@ -283,7 +283,7 @@ export class SemanticSearchService {
// Check if embedding needs regeneration
const shouldRegenerate = embeddingService.shouldRegenerateEmbedding(
note.content,
note.embedding,
note.embedding as any,
note.lastAiAnalysis
)
@@ -298,7 +298,7 @@ export class SemanticSearchService {
await prisma.note.update({
where: { id: noteId },
data: {
embedding: embeddingService.serialize(embedding),
embedding: embeddingService.serialize(embedding) as any,
lastAiAnalysis: new Date()
}
})

View File

@@ -1,41 +1,50 @@
import prisma from './prisma';
import prisma from './prisma'
import { unstable_cache } from 'next/cache'
const getCachedSystemConfig = unstable_cache(
async () => {
try {
const configs = await prisma.systemConfig.findMany()
return configs.reduce((acc, conf) => {
acc[conf.key] = conf.value
return acc
}, {} as Record<string, string>)
} catch (e) {
console.error('Failed to load system config from DB:', e)
return {}
}
},
['system-config'],
{ tags: ['system-config'] }
)
export async function getSystemConfig() {
try {
const configs = await prisma.systemConfig.findMany();
return configs.reduce((acc, conf) => {
acc[conf.key] = conf.value;
return acc;
}, {} as Record<string, string>);
} catch (e) {
console.error('Failed to load system config from DB:', e);
return {};
}
return getCachedSystemConfig()
}
/**
* Get a config value with a default fallback
*/
export async function getConfigValue(key: string, defaultValue: string = ''): Promise<string> {
const config = await getSystemConfig();
return config[key] || defaultValue;
const config = await getSystemConfig()
return config[key] || defaultValue
}
/**
* Get a numeric config value with a default fallback
*/
export async function getConfigNumber(key: string, defaultValue: number): Promise<number> {
const value = await getConfigValue(key, String(defaultValue));
const num = parseFloat(value);
return isNaN(num) ? defaultValue : num;
const value = await getConfigValue(key, String(defaultValue))
const num = parseFloat(value)
return isNaN(num) ? defaultValue : num
}
/**
* Get a boolean config value with a default fallback
*/
export async function getConfigBoolean(key: string, defaultValue: boolean): Promise<boolean> {
const value = await getConfigValue(key, String(defaultValue));
return value === 'true';
const value = await getConfigValue(key, String(defaultValue))
return value === 'true'
}
/**
@@ -52,4 +61,4 @@ export const SEARCH_DEFAULTS = {
QUERY_EXPANSION_ENABLED: false,
QUERY_EXPANSION_MAX_SYNONYMS: 3,
DEBUG_MODE: false,
} as const;
} as const

View File

@@ -1,62 +1,54 @@
/**
* Detect user's preferred language from their existing notes
* Analyzes language distribution across all user's notes
* Uses a single DB-level GROUP BY query — no note content is loaded
*/
import { auth } from '@/auth'
import { prisma } from '@/lib/prisma'
import { unstable_cache } from 'next/cache'
import { SupportedLanguage } from './load-translations'
const SUPPORTED_LANGUAGES = new Set(['en', 'fr', 'es', 'de', 'fa', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi', 'nl', 'pl'])
const getCachedUserLanguage = unstable_cache(
async (userId: string): Promise<SupportedLanguage> => {
try {
// Single aggregated query — no notes are fetched, only language counts
const result = await prisma.note.groupBy({
by: ['language'],
where: {
userId,
language: { not: null }
},
_sum: { languageConfidence: true },
_count: true,
orderBy: { _sum: { languageConfidence: 'desc' } },
take: 1,
})
if (result.length > 0 && result[0].language) {
const topLanguage = result[0].language as SupportedLanguage
if (SUPPORTED_LANGUAGES.has(topLanguage)) {
return topLanguage
}
}
return 'en'
} catch (error) {
console.error('Error detecting user language:', error)
return 'en'
}
},
['user-language'],
{ tags: ['user-language'] }
)
export async function detectUserLanguage(): Promise<SupportedLanguage> {
const session = await auth()
// Default to English for non-logged-in users
if (!session?.user?.id) {
return 'en'
}
try {
// Get all user's notes with detected languages
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
language: { not: null }
},
select: {
language: true,
languageConfidence: true
}
})
if (notes.length === 0) {
return 'en' // Default for new users
}
// Count language occurrences weighted by confidence
const languageScores: Record<string, number> = {}
for (const note of notes) {
if (note.language) {
const confidence = note.languageConfidence || 0.8
languageScores[note.language] = (languageScores[note.language] || 0) + confidence
}
}
// Find language with highest score
const sortedLanguages = Object.entries(languageScores)
.sort(([, a], [, b]) => b - a)
if (sortedLanguages.length > 0) {
const topLanguage = sortedLanguages[0][0] as SupportedLanguage
// Verify it's a supported language
if (['en', 'fr', 'es', 'de', 'fa', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi', 'nl', 'pl'].includes(topLanguage)) {
return topLanguage
}
}
return 'en'
} catch (error) {
console.error('Error detecting user language:', error)
return 'en'
}
return getCachedUserLanguage(session.user.id)
}

View File

@@ -34,18 +34,30 @@ export function deepEqual(a: unknown, b: unknown): boolean {
}
/**
* Parse a database note object into a typed Note
* Handles JSON string fields that are stored in the database
* Coerce a Prisma Json value into an array (or return fallback).
* Handles null, undefined, string (legacy JSON), object, etc.
*/
export function asArray<T = unknown>(val: unknown, fallback: T[] = []): T[] {
if (Array.isArray(val)) return val
if (typeof val === 'string') {
try { const p = JSON.parse(val); return Array.isArray(p) ? p : fallback } catch { return fallback }
}
return fallback
}
/**
* Parse a database note object into a typed Note.
* Guarantees array fields are always real arrays or null.
*/
export function parseNote(dbNote: any): Note {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
links: dbNote.links ? JSON.parse(dbNote.links) : null,
embedding: dbNote.embedding ? JSON.parse(dbNote.embedding) : null,
sharedWith: dbNote.sharedWith ? JSON.parse(dbNote.sharedWith) : [],
checkItems: asArray(dbNote.checkItems, null as any) ?? null,
labels: asArray(dbNote.labels) || null,
images: asArray(dbNote.images) || null,
links: asArray(dbNote.links) || null,
embedding: asArray<number>(dbNote.embedding) || null,
sharedWith: asArray(dbNote.sharedWith),
size: dbNote.size || 'small',
}
}