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:
73
memento-note/lib/blocks/extract-blocks.ts
Normal file
73
memento-note/lib/blocks/extract-blocks.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
export interface ExtractedBlock {
|
||||
blockId: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export function extractBlocksFromHtml(html: string): ExtractedBlock[] {
|
||||
const blocks: ExtractedBlock[] = []
|
||||
const regex = /<(?:p|h[1-6]|blockquote|li)[^>]*data-id="([^"]+)"[^>]*>([\s\S]*?)<\/(?:p|h[1-6]|blockquote|li)>/gi
|
||||
let match
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
const blockId = match[1]
|
||||
const content = match[2].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
if (content.length >= 10) {
|
||||
blocks.push({ blockId, content })
|
||||
}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
export function jaccardSimilarity(a: string, b: string): number {
|
||||
const tokenize = (s: string) =>
|
||||
new Set(
|
||||
s
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]/g, '')
|
||||
.split(/\s+/)
|
||||
.filter(w => w.length > 3)
|
||||
)
|
||||
const A = tokenize(a)
|
||||
const B = tokenize(b)
|
||||
if (A.size === 0 || B.size === 0) return 0
|
||||
let intersection = 0
|
||||
A.forEach(w => { if (B.has(w)) intersection++ })
|
||||
return intersection / (A.size + B.size - intersection)
|
||||
}
|
||||
|
||||
function extractPlainBlocksFromHtml(html: string): ExtractedBlock[] {
|
||||
const blocks: ExtractedBlock[] = []
|
||||
const regex = /<(?:p|h[1-6]|blockquote|li|td|th|div)[^>]*>([\s\S]*?)<\/(?:p|h[1-6]|blockquote|li|td|th|div)>/gi
|
||||
let match
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
const content = match[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
if (content.length >= 10) {
|
||||
blocks.push({ blockId: '', content })
|
||||
}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function pickBestFromBlocks(blocks: ExtractedBlock[], hint: string): ExtractedBlock | null {
|
||||
if (blocks.length === 0) return null
|
||||
if (!hint.trim()) return blocks[0]
|
||||
|
||||
let best = blocks[0]
|
||||
let bestScore = jaccardSimilarity(hint, best.content)
|
||||
for (const block of blocks.slice(1)) {
|
||||
const score = jaccardSimilarity(hint, block.content)
|
||||
if (score > bestScore) {
|
||||
best = block
|
||||
bestScore = score
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
export function pickBestBlockForHint(html: string, hint: string): ExtractedBlock | null {
|
||||
return pickBestFromBlocks(extractBlocksFromHtml(html), hint)
|
||||
}
|
||||
|
||||
/** Fallback when notes have no data-id yet (citation statique, pas de bloc vivant). */
|
||||
export function pickBestPlainPassageForHint(html: string, hint: string): ExtractedBlock | null {
|
||||
return pickBestFromBlocks(extractPlainBlocksFromHtml(html), hint)
|
||||
}
|
||||
Reference in New Issue
Block a user