feat(aep-v2): restore composants V2 manquants sur main (NavMapV2 + HashtagFilter + GraphView + FicheFamilleModal + types + css + server)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jules Neny
2026-05-07 00:56:08 +02:00
parent 9eb66ac10c
commit eb1bcf6080
12 changed files with 2398 additions and 13 deletions

View File

@@ -0,0 +1,96 @@
/**
* Recherche vectorielle sur les embeddings V2
* Cosine similarity + top-K
*
* Utilisé par : server/api/chatbot-v2.post.ts
* Données : server/data/embeddings-v2.json (généré par scripts/vectorize-v2.js)
*/
import { readFileSync, existsSync } from 'fs'
import { fileURLToPath } from 'url'
import { resolve, dirname } from 'path'
// ── Types ──────────────────────────────────────────────────────────────────────
export interface EmbeddingEntry {
fiche_id: string
nom: string
famille: number
hashtags: string[]
embedding: number[]
text_preview: string
}
export interface SearchResult {
fiche_id: string
nom: string
famille: number
hashtags: string[]
score: number
text_preview: string
}
// ── Cosine similarity ──────────────────────────────────────────────────────────
export function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) return 0
let dot = 0, normA = 0, normB = 0
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
const denom = Math.sqrt(normA) * Math.sqrt(normB)
return denom === 0 ? 0 : dot / denom
}
// ── Top-K search ───────────────────────────────────────────────────────────────
export function topKSearch(
embeddings: EmbeddingEntry[],
queryEmbedding: number[],
k: number = 5
): SearchResult[] {
return embeddings
.map(e => ({
fiche_id: e.fiche_id,
nom: e.nom,
famille: e.famille,
hashtags: e.hashtags,
score: cosineSimilarity(e.embedding, queryEmbedding),
text_preview: e.text_preview
}))
.sort((a, b) => b.score - a.score)
.slice(0, k)
}
// ── Chargement lazy des embeddings (cache module-level) ────────────────────────
let _embeddingsV2: EmbeddingEntry[] | null = null
export function loadEmbeddingsV2(): EmbeddingEntry[] {
if (_embeddingsV2 !== null) return _embeddingsV2
try {
// Résolution du chemin depuis server/utils/ vers server/data/
const currentDir = dirname(fileURLToPath(import.meta.url))
const embPath = resolve(currentDir, '..', 'data', 'embeddings-v2.json')
if (!existsSync(embPath)) {
console.warn('[vectorSearch] embeddings-v2.json absent - V2 vector search désactivé')
console.warn('[vectorSearch] Lancer : MISTRAL_API_KEY=xxx node scripts/vectorize-v2.js')
_embeddingsV2 = []
return []
}
const raw = readFileSync(embPath, 'utf-8')
const data = JSON.parse(raw)
_embeddingsV2 = data.embeddings ?? []
console.log(`[vectorSearch] ${_embeddingsV2!.length} embeddings V2 chargés (${data.meta?.model ?? 'unknown'})`)
return _embeddingsV2!
} catch (e: any) {
console.warn('[vectorSearch] Erreur chargement embeddings-v2.json :', e?.message ?? e)
_embeddingsV2 = []
return []
}
}