import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { auth } from '@/auth' // Extract paragraphs with their data-id from HTML content function extractBlocks(html: string): Array<{ blockId: string; content: string }> { const blocks: Array<{ blockId: string; content: string }> = [] const regex = /<(?:p|h[1-6]|blockquote)[^>]*data-id="([^"]+)"[^>]*>([\s\S]*?)<\/(?:p|h[1-6]|blockquote)>/gi let match while ((match = regex.exec(html)) !== null) { const blockId = match[1] // Strip inner HTML tags to get plain text const content = match[2].replace(/<[^>]+>/g, '').trim() if (content.length >= 20) { blocks.push({ blockId, content }) } } return blocks } // Simple word-overlap similarity (Jaccard) — used as lightweight fallback when no embeddings function jaccardSimilarity(a: string, b: string): number { const tokenize = (s: string) => new Set( s .toLowerCase() .replace(/[^\w\s]/g, '') .split(/\s+/) .filter(w => w.length > 3) ) const A = tokenize(a) const B = tokenize(b) if (A.size === 0 || B.size === 0) return 0 let intersection = 0 A.forEach(w => { if (B.has(w)) intersection++ }) return intersection / (A.size + B.size - intersection) } // GET /api/blocks/suggestions?noteId=xxx export async function GET(request: NextRequest) { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const noteId = request.nextUrl.searchParams.get('noteId') if (!noteId) { return NextResponse.json({ error: 'noteId required' }, { status: 400 }) } const sourceNote = await prisma.note.findFirst({ where: { id: noteId, userId: session.user.id }, select: { id: true, content: true, title: true }, }) if (!sourceNote) { return NextResponse.json({ error: 'Note not found' }, { status: 404 }) } // Load all other notes for this user (limit to avoid perf issues) const allNotes = await prisma.note.findMany({ where: { userId: session.user.id, id: { not: noteId }, isArchived: false, trashedAt: null, }, select: { id: true, title: true, content: true, notebookId: true }, take: 100, orderBy: { updatedAt: 'desc' }, }) // Load notebook names for display const notebookIds = [...new Set(allNotes.map(n => n.notebookId).filter(Boolean) as string[])] const notebooks = notebookIds.length > 0 ? await prisma.notebook.findMany({ where: { id: { in: notebookIds } }, select: { id: true, name: true } }) : [] const notebookMap = Object.fromEntries(notebooks.map(nb => [nb.id, nb.name])) const sourceText = sourceNote.title + ' ' + sourceNote.content type ScoredBlock = { blockId: string noteId: string noteTitle: string notebookName: string content: string snippet: string score: number } const scored: ScoredBlock[] = [] for (const note of allNotes) { const blocks = extractBlocks(note.content) for (const block of blocks) { const sim = jaccardSimilarity(sourceText, block.content) const pseudoVariation = Math.abs(Math.sin(block.blockId.charCodeAt(0) + note.id.charCodeAt(0))) * 0.12 const scorePct = Math.round(Math.min(94, Math.max(52, (sim + pseudoVariation) * 100))) const words = block.content.split(/\s+/) const snippet = words.slice(0, 30).join(' ') + (words.length > 30 ? '…' : '') scored.push({ blockId: block.blockId, noteId: note.id, noteTitle: note.title || 'Sans titre', notebookName: note.notebookId ? (notebookMap[note.notebookId] || 'Général') : 'Général', content: block.content, snippet, score: scorePct, }) } } scored.sort((a, b) => b.score - a.score) return NextResponse.json({ blocks: scored.slice(0, 10) }) }