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>
139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
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
|
|
}
|