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:
@@ -9,6 +9,7 @@ import {
|
||||
shouldCaptureHistorySnapshot,
|
||||
shouldCreateAutoSnapshot,
|
||||
} from '@/lib/note-history'
|
||||
import { syncNoteLinksForNote } from '@/lib/notes/sync-note-links'
|
||||
|
||||
// GET /api/notes/[id] - Get a single note
|
||||
export async function GET(
|
||||
@@ -167,9 +168,11 @@ export async function PUT(
|
||||
console.error('[HISTORY] Failed to create snapshot from /api/notes/[id] PUT:', snapshotError)
|
||||
}
|
||||
|
||||
// Fire-and-forget: sync [[wikilinks]] in background after content change
|
||||
// Fire-and-forget: sync note links after content change
|
||||
if ('content' in updateData) {
|
||||
syncNoteLinksBackground(id, session.user.id, note.content).catch(() => {})
|
||||
syncNoteLinksForNote(id, session.user.id, note.content).catch(err => {
|
||||
console.error('[NoteLink] sync failed after PUT:', err)
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -185,56 +188,6 @@ export async function PUT(
|
||||
}
|
||||
}
|
||||
|
||||
/** Background job: parse [[wikilinks]] and sync NoteLink table */
|
||||
async function syncNoteLinksBackground(noteId: string, userId: string, content: string) {
|
||||
const WIKILINK_RE = /\[\[([^\]|#]+?)(?:[|#][^\]]+)?\]\]/g
|
||||
const plain = content.replace(/<[^>]+>/g, ' ')
|
||||
const wikilinks: { 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())
|
||||
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()
|
||||
wikilinks.push({ title, snippet })
|
||||
}
|
||||
|
||||
const upsertedIds: string[] = []
|
||||
|
||||
for (const { title, snippet } of wikilinks) {
|
||||
let targetNote = await prisma.note.findFirst({
|
||||
where: { userId, title: { equals: title, mode: 'insensitive' }, trashedAt: null },
|
||||
select: { id: true },
|
||||
})
|
||||
if (!targetNote) {
|
||||
targetNote = await prisma.note.create({
|
||||
data: { title, content: '', userId, type: 'richtext', color: 'default', isMarkdown: true, order: 0 },
|
||||
select: { id: true },
|
||||
})
|
||||
}
|
||||
if (targetNote.id === noteId) continue
|
||||
await (prisma as any).noteLink.upsert({
|
||||
where: { sourceNoteId_targetNoteId: { sourceNoteId: noteId, targetNoteId: targetNote.id } },
|
||||
update: { contextSnippet: snippet.slice(0, 200) },
|
||||
create: { id: crypto.randomUUID(), sourceNoteId: noteId, targetNoteId: targetNote.id, contextSnippet: snippet.slice(0, 200) },
|
||||
})
|
||||
upsertedIds.push(targetNote.id)
|
||||
}
|
||||
|
||||
if (upsertedIds.length > 0) {
|
||||
await (prisma as any).noteLink.deleteMany({
|
||||
where: { sourceNoteId: noteId, targetNoteId: { notIn: upsertedIds } },
|
||||
})
|
||||
} else {
|
||||
await (prisma as any).noteLink.deleteMany({ where: { sourceNoteId: noteId } })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/notes/[id] - Delete a note
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
|
||||
Reference in New Issue
Block a user