import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import prisma from '@/lib/prisma' import { clusteringService } from '@/lib/ai/services/clustering.service' import { bridgeNotesService } from '@/lib/ai/services/bridge-notes.service' /** * GET /api/clusters * Get all clusters for the current user. * Returns cached clusters + bridge notes enriched with cluster names. */ export async function GET(request: NextRequest) { try { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = session.user.id // Check for stored results (even if stale/périmés) const stored = await clusteringService.getStoredClusters(userId) if (stored) { const cached = stored.clusters // Fetch notes with their cluster assignments const notes = await prisma.note.findMany({ where: { userId, trashedAt: null }, select: { id: true, title: true, content: true } }) // Get cluster member mappings const clusterMembers = await prisma.clusterMember.findMany({ where: { userId }, select: { noteId: true, clusterId: true, isCentral: true } }) const noteClusterMap = new Map(clusterMembers.map(cm => [cm.noteId, { clusterId: cm.clusterId, isCentral: cm.isCentral }])) const notesWithClusters = notes.map(n => { const mapping = noteClusterMap.get(n.id) return { ...n, clusterId: mapping?.clusterId, isCentral: mapping?.isCentral || false } }) // Fetch bridge notes with enrichment (cluster names + note details) const bridgeNotesData = await prisma.bridgeNote.findMany({ where: { userId }, orderBy: { bridgeScore: 'desc' }, take: 10 }) let enrichedBridgeNotes: object[] = [] if (bridgeNotesData.length > 0) { const bridgeNoteIds = bridgeNotesData.map(b => b.noteId) const bridgeNoteDetails = await prisma.note.findMany({ where: { id: { in: bridgeNoteIds } }, select: { id: true, title: true, content: true } }) const bridgeNoteDetailsMap = new Map(bridgeNoteDetails.map(n => [n.id, n])) enrichedBridgeNotes = bridgeNotesData.map(b => { const clustersConnected = JSON.parse(b.clustersConnected) as number[] return { noteId: b.noteId, bridgeScore: b.bridgeScore, clustersConnected, clusterNames: clustersConnected.map( cid => cached.find(c => c.clusterId === cid)?.name || `Cluster ${cid}` ), note: bridgeNoteDetailsMap.get(b.noteId) } }) } const embeddingCountRow = await prisma.$queryRawUnsafe>( `SELECT COUNT(*) FROM "NoteEmbedding" ne INNER JOIN "Note" n ON n.id = ne."noteId" WHERE n."userId" = $1 AND n."trashedAt" IS NULL`, userId ) return NextResponse.json({ clusters: cached, notes: notesWithClusters, bridgeNotes: enrichedBridgeNotes, cached: true, stale: stored.stale, lastCalculated: stored.lastCalculated, totalNotes: notes.length, embeddingCount: Number(embeddingCountRow[0]?.count || 0), }) } // No cached results - return info about notes const notesCount = await prisma.note.count({ where: { userId, trashedAt: null } }) const embeddingCount = await prisma.$queryRawUnsafe>( `SELECT COUNT(*) FROM "NoteEmbedding" ne INNER JOIN "Note" n ON n.id = ne."noteId" WHERE n."userId" = $1 AND n."trashedAt" IS NULL`, userId ) return NextResponse.json({ clusters: [], notes: [], bridgeNotes: [], totalNotes: notesCount, embeddingCount: Number(embeddingCount[0]?.count || 0), needsCalculation: true }) } catch (error) { console.error('Error fetching clusters:', error) return NextResponse.json( { error: 'Failed to fetch clusters' }, { status: 500 } ) } } /** * POST /api/clusters * Trigger a full recalculation of clusters and bridge notes. * Returns clusters + bridge notes enriched for immediate display. */ export async function POST(request: NextRequest) { try { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = session.user.id const body = await request.json().catch(() => ({})) const force = Boolean(body?.force) // 0. Indexer / réindexer les embeddings (texte complet, multi-chunks) await clusteringService.ensureEmbeddings(userId, { force: force || true }) // 1. Run DBSCAN clustering const results = await clusteringService.clusterNotes(userId) if (results.clusters.length === 0) { return NextResponse.json({ clusters: [], bridgeNotes: [], message: 'Could not generate clusters. Notes may be too diverse or not enough.', noiseCount: results.noiseCount }) } // 2. Generate cluster names with AI for (const cluster of results.clusters) { cluster.name = await clusteringService.generateClusterName(cluster.clusterId, userId) } // 3. Save clustering results await clusteringService.saveClusteringResults(userId, results) // 4. Detect and save bridge notes const bridgeNotes = await bridgeNotesService.detectBridgeNotes(userId) await bridgeNotesService.saveBridgeNotes(userId, bridgeNotes) // 5. Generate and save bridge suggestions const suggestions = await bridgeNotesService.generateBridgeSuggestions(userId) await bridgeNotesService.saveBridgeSuggestions(userId, suggestions) // 6. Fetch notes with cluster assignments const notes = await prisma.note.findMany({ where: { userId, trashedAt: null }, select: { id: true, title: true, content: true } }) const clusterMembers = await prisma.clusterMember.findMany({ where: { userId }, select: { noteId: true, clusterId: true, isCentral: true } }) const noteClusterMap = new Map(clusterMembers.map(cm => [cm.noteId, { clusterId: cm.clusterId, isCentral: cm.isCentral }])) const notesWithClusters = notes.map(n => { const mapping = noteClusterMap.get(n.id) return { ...n, clusterId: mapping?.clusterId, isCentral: mapping?.isCentral || false } }) // 7. Enrich bridge notes with cluster names + note details const noteMap = new Map(notes.map(n => [n.id, n])) const enrichedBridgeNotes = bridgeNotes.slice(0, 10).map(b => ({ noteId: b.noteId, bridgeScore: b.bridgeScore, clustersConnected: b.clustersConnected, clusterNames: b.clusterNames, note: noteMap.get(b.noteId) })) return NextResponse.json({ clusters: results.clusters, notes: notesWithClusters, bridgeNotes: enrichedBridgeNotes, totalNotes: results.clusters.reduce((sum, c) => sum + c.noteIds.length, 0) + results.noiseCount, noiseCount: results.noiseCount, message: `Generated ${results.clusters.length} clusters with ${bridgeNotes.length} bridge notes` }) } catch (error) { console.error('Error recalculating clusters:', error) return NextResponse.json( { error: 'Failed to recalculate clusters', details: String(error) }, { status: 500 } ) } }