import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { auth } from '@/auth' import { memoryEchoService } from '@/lib/ai/services/memory-echo.service' import { hasUserAiConsent } from '@/lib/consent/server-consent' import { SEMANTIC_SIMILARITY_FLOOR, SEMANTIC_SIMILARITY_FLOOR_DEMO } from '@/lib/ai/semantic-proximity' import { extractNoteLinkTargets } from '@/lib/notes/sync-note-links' function excerpt(text: string, max = 120): string { const plain = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() if (plain.length <= max) return plain return `${plain.slice(0, max).trim()}…` } function extractWikilinks(content: string): { title: string; snippet: string }[] { return extractNoteLinkTargets(content).map(t => ({ title: t.title, snippet: t.snippet, })) } export async function GET( _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const { id } = await params const note = await prisma.note.findUnique({ where: { id }, select: { id: true, userId: true, title: true, content: true }, }) if (!note || note.userId !== session.user.id) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } let backlinks: Awaited> = [] let outbound: Awaited> = [] try { ;[backlinks, outbound] = await Promise.all([ prisma.noteLink.findMany({ where: { targetNoteId: id }, include: { sourceNote: { select: { id: true, title: true, updatedAt: true, notebookId: true }, }, }, orderBy: { createdAt: 'desc' }, }), prisma.noteLink.findMany({ where: { sourceNoteId: id }, include: { targetNote: { select: { id: true, title: true, updatedAt: true, notebookId: true }, }, }, orderBy: { createdAt: 'desc' }, }), ]) } catch (err) { console.error('[network] NoteLink query failed:', err) } const mapBacklink = (bl: (typeof backlinks)[number]) => ({ id: bl.id, note: bl.sourceNote, contextSnippet: bl.contextSnippet, createdAt: bl.createdAt, }) const mapOutbound = (ol: (typeof outbound)[number]) => ({ id: ol.id, note: ol.targetNote, contextSnippet: ol.contextSnippet, createdAt: ol.createdAt, }) const backlinkRows = backlinks.map(mapBacklink) const outboundRows = outbound.map(mapOutbound) const linkedTitles = new Set( outboundRows.map(link => (link.note?.title || '').toLowerCase()).filter(Boolean) ) const wikilinks = extractWikilinks(note.content || '') const unlinkedMentions = wikilinks .filter(w => !linkedTitles.has(w.title.toLowerCase())) .map(w => ({ title: w.title, snippet: w.snippet, })) const liveBlockRefs = await prisma.liveBlockRef.findMany({ where: { sourceNoteId: id }, orderBy: { createdAt: 'desc' }, select: { id: true, blockId: true, targetNoteId: true, targetNote: { select: { id: true, title: true, notebookId: true }, }, }, }) const embedHosts = new Map() for (const ref of liveBlockRefs) { const existing = embedHosts.get(ref.targetNoteId) if (existing) { if (!existing.blockIds.includes(ref.blockId)) existing.blockIds.push(ref.blockId) continue } embedHosts.set(ref.targetNoteId, { note: ref.targetNote, blockIds: [ref.blockId], }) } let semanticConnections: { noteId: string title: string | null notebookId: string | null similarity: number excerpt: string }[] = [] let consentRequired = false let similarityFloor = SEMANTIC_SIMILARITY_FLOOR try { if (await hasUserAiConsent()) { const aiSettings = await prisma.userAISettings.findUnique({ where: { userId: session.user.id }, select: { demoMode: true }, }) similarityFloor = aiSettings?.demoMode ? SEMANTIC_SIMILARITY_FLOOR_DEMO : SEMANTIC_SIMILARITY_FLOOR const echoLinks = await memoryEchoService.getConnectionsForNote(id, session.user.id) const otherIds = echoLinks.map(conn => (conn.note1.id === id ? conn.note2.id : conn.note1.id)) const notebookRows = otherIds.length > 0 ? await prisma.note.findMany({ where: { id: { in: otherIds }, userId: session.user.id }, select: { id: true, notebookId: true }, }) : [] const notebookByNote = Object.fromEntries(notebookRows.map(n => [n.id, n.notebookId])) semanticConnections = echoLinks.map(conn => { const isNote1Target = conn.note1.id === id const other = isNote1Target ? conn.note2 : conn.note1 return { noteId: other.id, title: other.title, notebookId: notebookByNote[other.id] ?? null, similarity: conn.similarityScore, excerpt: excerpt(other.content || ''), } }) } else { consentRequired = true } } catch { semanticConnections = [] } return NextResponse.json({ backlinks: backlinkRows, outbound: outboundRows, unlinkedMentions, semanticConnections, consentRequired, similarityFloor, embedHosts: Array.from(embedHosts.values()).map(entry => ({ note: entry.note, blockIds: entry.blockIds, })), }) }