feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped

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:
Antigravity
2026-05-24 14:27:29 +00:00
parent 077e665dfc
commit e2672cd2c2
323 changed files with 20670 additions and 42431 deletions

View File

@@ -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 })
}