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:
@@ -1,42 +1,11 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import { syncNoteLinksForNote } from '@/lib/notes/sync-note-links'
|
||||
|
||||
const WIKILINK_RE = /\[\[([^\]|#]+?)(?:[|#][^\]]+)?\]\]/g
|
||||
|
||||
/**
|
||||
* Extract [[wikilink]] targets from markdown/html content.
|
||||
* Returns deduplicated list of linked note titles.
|
||||
*/
|
||||
function extractWikilinks(content: string): { title: string; snippet: string }[] {
|
||||
// Strip HTML tags
|
||||
const plain = content.replace(/<[^>]+>/g, ' ')
|
||||
const results: { title: string; snippet: string }[] = []
|
||||
const seen = new Set<string>()
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
WIKILINK_RE.lastIndex = 0
|
||||
while ((match = WIKILINK_RE.exec(plain)) !== null) {
|
||||
const title = match[1].trim()
|
||||
if (!title || seen.has(title.toLowerCase())) continue
|
||||
seen.add(title.toLowerCase())
|
||||
|
||||
// Extract snippet: 50 chars before + after the match
|
||||
const start = Math.max(0, match.index - 50)
|
||||
const end = Math.min(plain.length, match.index + match[0].length + 50)
|
||||
const snippet = plain.slice(start, end).replace(/\s+/g, ' ').trim()
|
||||
|
||||
results.push({ title, snippet })
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// POST /api/notes/[id]/sync-links
|
||||
// Parse [[wikilinks]] in note content and sync the NoteLink table.
|
||||
// Called automatically after note save.
|
||||
// POST /api/notes/[id]/sync-links — resynchronise les liens internes depuis le contenu
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth()
|
||||
@@ -56,75 +25,6 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const wikilinks = extractWikilinks(note.content)
|
||||
|
||||
// For each wikilink, find or create the target note
|
||||
const upsertedLinks: string[] = []
|
||||
|
||||
for (const { title, snippet } of wikilinks) {
|
||||
// Find target note by title (case-insensitive) belonging to same user
|
||||
let targetNote = await prisma.note.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
title: { equals: title, mode: 'insensitive' },
|
||||
trashedAt: null,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (!targetNote) {
|
||||
// Create a stub note so the link resolves
|
||||
targetNote = await prisma.note.create({
|
||||
data: {
|
||||
title,
|
||||
content: '',
|
||||
userId,
|
||||
type: 'richtext',
|
||||
color: 'default',
|
||||
isMarkdown: true,
|
||||
order: 0,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
}
|
||||
|
||||
// Skip self-links
|
||||
if (targetNote.id === id) continue
|
||||
|
||||
// Upsert the NoteLink
|
||||
await (prisma as any).noteLink.upsert({
|
||||
where: {
|
||||
sourceNoteId_targetNoteId: {
|
||||
sourceNoteId: id,
|
||||
targetNoteId: targetNote.id,
|
||||
},
|
||||
},
|
||||
update: { contextSnippet: snippet.slice(0, 200) },
|
||||
create: {
|
||||
id: crypto.randomUUID(),
|
||||
sourceNoteId: id,
|
||||
targetNoteId: targetNote.id,
|
||||
contextSnippet: snippet.slice(0, 200),
|
||||
},
|
||||
})
|
||||
|
||||
upsertedLinks.push(targetNote.id)
|
||||
}
|
||||
|
||||
// Delete obsolete links (links that existed before but wikilink was removed)
|
||||
if (upsertedLinks.length > 0) {
|
||||
await (prisma as any).noteLink.deleteMany({
|
||||
where: {
|
||||
sourceNoteId: id,
|
||||
targetNoteId: { notIn: upsertedLinks },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// No wikilinks at all — remove all outgoing links from this note
|
||||
await (prisma as any).noteLink.deleteMany({
|
||||
where: { sourceNoteId: id },
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ synced: upsertedLinks.length })
|
||||
const synced = await syncNoteLinksForNote(id, userId, note.content || '')
|
||||
return NextResponse.json({ synced })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user