feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,31 +20,50 @@ export async function GET(request: NextRequest) {
|
||||
// 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, check if user has enough notes
|
||||
// No cached results - return info about notes
|
||||
const notesCount = await prisma.note.count({
|
||||
where: { userId, trashedAt: null }
|
||||
})
|
||||
|
||||
if (notesCount < 10) {
|
||||
return NextResponse.json({
|
||||
clusters: [],
|
||||
message: 'Need at least 10 notes to generate clusters',
|
||||
totalNotes: notesCount
|
||||
})
|
||||
}
|
||||
const embeddingCount = await prisma.$queryRawUnsafe<Array<{ count: bigint }>>(
|
||||
`SELECT COUNT(*) FROM "NoteEmbedding" ne
|
||||
INNER JOIN "Note" n ON n.id = ne."noteId"
|
||||
WHERE n."userId" = $1 AND n."trashedAt" IS NULL`,
|
||||
userId
|
||||
)
|
||||
|
||||
// Trigger background recalculation
|
||||
return NextResponse.json({
|
||||
clusters: [],
|
||||
message: 'Calculating clusters... Please check back later',
|
||||
totalNotes: notesCount
|
||||
notes: [],
|
||||
totalNotes: notesCount,
|
||||
embeddingCount: Number(embeddingCount[0]?.count || 0),
|
||||
needsCalculation: true
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching clusters:', error)
|
||||
@@ -56,7 +75,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/clusters/recalculate
|
||||
* POST /api/clusters
|
||||
* Trigger a full recalculation of clusters and bridge notes.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -67,57 +86,73 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
const body = await request.json()
|
||||
const force = body.force === true
|
||||
|
||||
// Check if recalculation is needed
|
||||
const shouldRecalc = force || await clusteringService.shouldRecalculate(userId)
|
||||
|
||||
if (!shouldRecalc) {
|
||||
const cached = await clusteringService.getCachedClusters(userId)
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
clusters: cached,
|
||||
cached: true,
|
||||
message: 'Using cached results (data has not changed significantly)'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Perform clustering
|
||||
// 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. Need more diverse notes.',
|
||||
message: 'Could not generate clusters. Notes may be too diverse or not enough.',
|
||||
noiseCount: results.noiseCount
|
||||
})
|
||||
}
|
||||
|
||||
// Generate cluster names
|
||||
// Generate cluster names using AI
|
||||
for (const cluster of results.clusters) {
|
||||
cluster.name = await clusteringService.generateClusterName(cluster.clusterId, userId)
|
||||
}
|
||||
|
||||
// Save results
|
||||
// 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,
|
||||
bridgeNotes: bridgeNotes.slice(0, 10), // Return top 10
|
||||
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`
|
||||
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' },
|
||||
{ error: 'Failed to recalculate clusters', details: String(error) },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user