chore: snapshot before performance optimization
This commit is contained in:
@@ -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 },
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user