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. */ 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 cached results const cached = await clusteringService.getCachedClusters(userId) if (cached) { // 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 } }) const noteClusterMap = new Map(clusterMembers.map(cm => [cm.noteId, cm.clusterId])) const notesWithClusters = notes.map(n => ({ ...n, clusterId: noteClusterMap.get(n.id) })) return NextResponse.json({ clusters: cached, notes: notesWithClusters, cached: true, totalNotes: cached.reduce((sum, c) => sum + c.noteIds.length, 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: [], 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. */ 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 // Use the PROPER clustering service (DBSCAN algorithm) const results = await clusteringService.clusterNotes(userId) if (results.clusters.length === 0) { return NextResponse.json({ clusters: [], message: 'Could not generate clusters. Notes may be too diverse or not enough.', noiseCount: results.noiseCount }) } // Generate cluster names using AI for (const cluster of results.clusters) { cluster.name = await clusteringService.generateClusterName(cluster.clusterId, userId) } // Save clustering results await clusteringService.saveClusteringResults(userId, results) // Detect and save bridge notes const bridgeNotes = await bridgeNotesService.detectBridgeNotes(userId) await bridgeNotesService.saveBridgeNotes(userId, bridgeNotes) // Generate and save bridge suggestions const suggestions = await bridgeNotesService.generateBridgeSuggestions(userId) await bridgeNotesService.saveBridgeSuggestions(userId, suggestions) // 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 } }) const noteClusterMap = new Map(clusterMembers.map(cm => [cm.noteId, cm.clusterId])) const notesWithClusters = notes.map(n => ({ ...n, clusterId: noteClusterMap.get(n.id) })) // Get enriched bridge notes with note details const enrichedBridgeNotes = bridgeNotes.slice(0, 10).map(b => ({ noteId: b.noteId, bridgeScore: b.bridgeScore, clustersConnected: b.clustersConnected, clusterNames: b.clusterNames, note: notes.find(n => n.id === 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 } ) } }