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:
138
memento-note/lib/notes/sync-note-links.ts
Normal file
138
memento-note/lib/notes/sync-note-links.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
/** [[Titre]] ou [[Titre|noteId]] */
|
||||
const WIKILINK_RE = /\[\[([^\]|#]+?)(?:\|([^\]|#]+?))?(?:[#][^\]]+)?\]\]/g
|
||||
|
||||
const OPEN_NOTE_IN_HREF_RE = /href\s*=\s*["']([^"']*openNote=([^"'&#]+)[^"']*)["']/gi
|
||||
|
||||
export interface ParsedNoteLinkTarget {
|
||||
title: string
|
||||
noteId?: string
|
||||
snippet: string
|
||||
}
|
||||
|
||||
export function extractNoteLinkTargets(content: string): ParsedNoteLinkTarget[] {
|
||||
const results: ParsedNoteLinkTarget[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
const push = (title: string, noteId: string | undefined, snippet: string) => {
|
||||
const key = noteId ? `id:${noteId}` : `title:${title.toLowerCase()}`
|
||||
if (!title && !noteId) return
|
||||
if (seen.has(key)) return
|
||||
seen.add(key)
|
||||
results.push({ title: title || 'Sans titre', noteId, snippet })
|
||||
}
|
||||
|
||||
const plain = content.replace(/<[^>]+>/g, ' ')
|
||||
let match: RegExpExecArray | null
|
||||
WIKILINK_RE.lastIndex = 0
|
||||
while ((match = WIKILINK_RE.exec(plain)) !== null) {
|
||||
const title = match[1].trim()
|
||||
const noteId = match[2]?.trim()
|
||||
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()
|
||||
push(title, noteId, snippet)
|
||||
}
|
||||
|
||||
OPEN_NOTE_IN_HREF_RE.lastIndex = 0
|
||||
while ((match = OPEN_NOTE_IN_HREF_RE.exec(content)) !== null) {
|
||||
const href = match[1]
|
||||
const noteId = decodeURIComponent(match[2].trim())
|
||||
const idx = content.indexOf(href)
|
||||
const start = Math.max(0, idx - 50)
|
||||
const end = Math.min(content.length, idx + href.length + 50)
|
||||
const snippet = content.slice(start, end).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
push('', noteId, snippet)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/** Parse [[wikilinks]] + liens internes openNote, synchronise la table NoteLink. */
|
||||
export async function syncNoteLinksForNote(
|
||||
noteId: string,
|
||||
userId: string,
|
||||
content: string,
|
||||
): Promise<number> {
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id: noteId },
|
||||
select: { id: true, userId: true },
|
||||
})
|
||||
if (!note || note.userId !== userId) return 0
|
||||
|
||||
const targets = extractNoteLinkTargets(content)
|
||||
const upsertedIds: string[] = []
|
||||
|
||||
for (const target of targets) {
|
||||
let targetNoteId = target.noteId
|
||||
|
||||
if (targetNoteId) {
|
||||
const byId = await prisma.note.findFirst({
|
||||
where: { id: targetNoteId, userId, trashedAt: null },
|
||||
select: { id: true },
|
||||
})
|
||||
if (!byId) {
|
||||
if (!target.title) continue
|
||||
targetNoteId = undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetNoteId && target.title) {
|
||||
let targetNote = await prisma.note.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
title: { equals: target.title, mode: 'insensitive' },
|
||||
trashedAt: null,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (!targetNote) {
|
||||
targetNote = await prisma.note.create({
|
||||
data: {
|
||||
title: target.title,
|
||||
content: '',
|
||||
userId,
|
||||
type: 'richtext',
|
||||
color: 'default',
|
||||
isMarkdown: false,
|
||||
order: 0,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
}
|
||||
targetNoteId = targetNote.id
|
||||
}
|
||||
|
||||
if (!targetNoteId || targetNoteId === noteId) continue
|
||||
|
||||
await prisma.noteLink.upsert({
|
||||
where: {
|
||||
sourceNoteId_targetNoteId: {
|
||||
sourceNoteId: noteId,
|
||||
targetNoteId,
|
||||
},
|
||||
},
|
||||
update: { contextSnippet: target.snippet.slice(0, 200) },
|
||||
create: {
|
||||
sourceNoteId: noteId,
|
||||
targetNoteId,
|
||||
contextSnippet: target.snippet.slice(0, 200),
|
||||
},
|
||||
})
|
||||
upsertedIds.push(targetNoteId)
|
||||
}
|
||||
|
||||
if (upsertedIds.length > 0) {
|
||||
await prisma.noteLink.deleteMany({
|
||||
where: {
|
||||
sourceNoteId: noteId,
|
||||
targetNoteId: { notIn: upsertedIds },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await prisma.noteLink.deleteMany({ where: { sourceNoteId: noteId } })
|
||||
}
|
||||
|
||||
return upsertedIds.length
|
||||
}
|
||||
Reference in New Issue
Block a user