diff --git a/components/ChatbotPlaceholder.vue b/components/ChatbotPlaceholder.vue
index 7b41141..1f7d9e0 100644
--- a/components/ChatbotPlaceholder.vue
+++ b/components/ChatbotPlaceholder.vue
@@ -27,7 +27,7 @@
- {{ isExpanded ? title : placeholder }}
+ {{ isExpanded ? 'Chatbot AEP' : 'Pose une question sur le réseau…' }}
@@ -52,20 +52,9 @@
-
- Ce chatbot fonctionne sur un serveur européen souverain
-(Mistral FR, zéro rétention), conçu sobre en énergie.
- Pour m'aider à te répondre efficacement,
-formule ta requête ainsi :
-
- - • Besoin : [ce que tu cherches]
- - • Thématique : [juridique / technique / économique / ...]
- - • Lieu : [région ou ville]
-
- Exemple : "Je suis salarié d'agence, litige avec mon
-employeur, besoin conseil juridique droit du travail,
-Île-de-France."
-
+
Explore les 120 structures de la carte par la conversation. Je peux t'aider à trouver des collectifs, agences ou réseaux selon ta situation, ta pratique ou tes inspirations du moment.
+
Exemple : "Je cherche des acteurs de la rénovation de maisons individuelles en France, plutôt en milieu rural, avec des approches biosourcées ou low-tech."
+
Propulsé par Mistral FR - serveur européen souverain, zéro rétention.
@@ -74,17 +63,32 @@ employeur, besoin conseil juridique droit du travail,
{{ msg.content }}
+
+
Filtrer par :
+
+ {{ tag }}
+
+
@@ -134,22 +138,12 @@ interface ChatMessage {
role: 'user' | 'assistant'
content: string
fiches?: FicheReco[]
+ suggestedHashtags?: string[]
}
-const props = withDefaults(defineProps<{
- endpoint?: string
- title?: string
- placeholder?: string
- ficheBasePath?: string
-}>(), {
- endpoint: '/api/chatbot',
- title: 'Chatbot AEP',
- placeholder: 'Pose une question sur le réseau…',
- ficheBasePath: '/fiche',
-})
-
const emit = defineEmits<{
'highlightOrgs': [ids: (number | string)[]]
+ 'applyHashtag': [tag: string]
}>()
const isExpanded = ref(false)
@@ -159,6 +153,37 @@ const loading = ref(false)
const errorMsg = ref('')
const messagesContainer = ref
(null)
+// Detection hashtags depuis la question posee
+const HASHTAG_KEYWORDS: Record = {
+ '#reemploi-structurel': ['reemploi', 'materiaux recuperes', 'deconstruction', 'reemploi structurel'],
+ '#reemploi-second-oeuvre': ['revetement', 'second oeuvre', 'reemploi'],
+ '#biosource-geosource': ['biosource', 'geosource', 'paille', 'terre', 'chanvre', 'lin', 'biosource'],
+ '#low-tech-experimentation': ['low-tech', 'low tech', 'technique simple', 'autonomie', 'lowtech'],
+ '#chantier-ecole': ['formation', 'chantier ecole', 'chantier-ecole', 'apprendre', 'auto-construction', 'autoconstruction'],
+ '#sobriete-energetique': ['sobriete', 'energie', 'renovation energetique', 'isolation', 'chauffage', 'economie energie'],
+ '#mal-logement-precarite': ['mal-logement', 'precarite', 'sans-abri', 'logement social', 'squat', 'mal logement'],
+ '#tiers-lieux-friches': ['friche', 'tiers-lieu', 'tiers lieu', 'espace intermediaire', 'temporaire', 'reconversion'],
+ '#accompagnement-cooperatif': ['cooperative', 'accompagnement', 'cooperation', 'collectif', 'mutualisation'],
+ '#transition-energetique-territoriale': ['territoire', 'transition', 'energetique', 'local', 'region', 'transition energetique'],
+ '#communs-fonciers': ['communs', 'foncier', 'anti-speculatif', 'community land trust', 'commun foncier'],
+ '#hack-juridique': ['juridique', 'montage', 'structure legale', 'sci', 'cooperative', 'statut'],
+ '#retrofit-strates': ['retrofit', 'renovation lourde', 'sur-isolation', 'rehaussement'],
+ '#phytoconstruction': ['plantes', 'vegetal', 'arbre', 'construction vivante', 'phyto'],
+}
+
+function detectHashtagsFromQuery(query: string): string[] {
+ const q = query.toLowerCase()
+ .normalize('NFD')
+ .replace(/[̀-ͯ]/g, '')
+ const detected: string[] = []
+ for (const [hashtag, keywords] of Object.entries(HASHTAG_KEYWORDS)) {
+ if (keywords.some(kw => q.includes(kw))) {
+ detected.push(hashtag)
+ }
+ }
+ return detected.slice(0, 3)
+}
+
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
@@ -179,15 +204,17 @@ async function sendMessage() {
const res = await $fetch<{
reponse_texte: string
fiches_recommandees: { id: number | string; nom: string; explication: string }[]
- }>(props.endpoint, {
+ }>('/api/chatbot-v2', {
method: 'POST',
body: { question },
})
+ const suggestedHashtags = detectHashtagsFromQuery(question)
const assistantMsg: ChatMessage = {
role: 'assistant',
content: res.reponse_texte,
fiches: res.fiches_recommandees || [],
+ suggestedHashtags: suggestedHashtags.length ? suggestedHashtags : undefined,
}
messages.value.push(assistantMsg)
diff --git a/scripts/vectorize-v2.cjs b/scripts/vectorize-v2.cjs
new file mode 100644
index 0000000..99cdb3b
--- /dev/null
+++ b/scripts/vectorize-v2.cjs
@@ -0,0 +1,127 @@
+// scripts/vectorize-v2.js
+// Usage : MISTRAL_API_KEY=xxx node scripts/vectorize-v2.js
+// Génère : server/data/embeddings-v2.json
+//
+// SETUP AVANT DEPLOY :
+// cd nav-carte && MISTRAL_API_KEY=xxx node scripts/vectorize-v2.js
+// Coût estimé : ~0.10 EUR pour 120 fiches
+//
+// Prérequis : Node >= 18 (fetch natif disponible)
+
+const fs = require('fs')
+const path = require('path')
+
+const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY
+if (!MISTRAL_API_KEY) {
+ console.error('Erreur : MISTRAL_API_KEY manquante')
+ console.error('Usage : MISTRAL_API_KEY=xxx node scripts/vectorize-v2.js')
+ process.exit(1)
+}
+
+const dataPath = path.join(process.cwd(), 'public', 'data', 'reseaux-bifurcation.json')
+const outPath = path.join(process.cwd(), 'server', 'data', 'embeddings-v2.json')
+
+// Créer server/data/ si absent
+const outDir = path.dirname(outPath)
+if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true })
+
+const rawData = fs.readFileSync(dataPath, 'utf-8')
+const data = JSON.parse(rawData)
+const structures = data.structures
+
+if (!Array.isArray(structures) || structures.length === 0) {
+ console.error('Erreur : aucune structure trouvée dans reseaux-bifurcation.json')
+ process.exit(1)
+}
+
+async function embedBatch(texts) {
+ const res = await fetch('https://api.mistral.ai/v1/embeddings', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${MISTRAL_API_KEY}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: 'mistral-embed',
+ input: texts
+ })
+ })
+ if (!res.ok) {
+ const err = await res.text()
+ throw new Error(`Mistral API error ${res.status}: ${err}`)
+ }
+ const json = await res.json()
+ return json.data.map(d => d.embedding)
+}
+
+function buildText(s) {
+ const parts = [
+ s.nom,
+ s.description_courte ?? '',
+ (s.description_longue ?? '').slice(0, 800),
+ (s.hashtags ?? []).join(' '),
+ (s.sources ?? []).map(src => src.titre).join(' '),
+ (s.pensees ?? []).map(p => p.label).join(' ')
+ ]
+ return parts.filter(Boolean).join('\n\n')
+}
+
+async function main() {
+ const embeddings = []
+ const BATCH_SIZE = 8 // Mistral embed : rate limit prudent
+
+ console.log(`Vectorisation de ${structures.length} structures (modele : mistral-embed)...`)
+ console.log(`Sortie : ${outPath}`)
+ console.log()
+
+ for (let i = 0; i < structures.length; i += BATCH_SIZE) {
+ const batch = structures.slice(i, i + BATCH_SIZE)
+ const texts = batch.map(buildText)
+
+ try {
+ const batchEmbeddings = await embedBatch(texts)
+ batch.forEach((s, j) => {
+ embeddings.push({
+ fiche_id: s.id,
+ nom: s.nom,
+ famille: s.famille_principale,
+ hashtags: s.hashtags ?? [],
+ embedding: batchEmbeddings[j],
+ text_preview: texts[j].slice(0, 300)
+ })
+ })
+ const batchNum = Math.floor(i / BATCH_SIZE) + 1
+ const totalBatches = Math.ceil(structures.length / BATCH_SIZE)
+ console.log(` Batch ${batchNum}/${totalBatches} OK (${batch.length} fiches)`)
+ // Pause rate limit entre batches
+ await new Promise(r => setTimeout(r, 200))
+ } catch (err) {
+ console.error(` Erreur batch ${i}-${i + BATCH_SIZE}:`, err.message)
+ process.exit(1)
+ }
+ }
+
+ const output = {
+ meta: {
+ total: embeddings.length,
+ model: 'mistral-embed',
+ date: new Date().toISOString(),
+ source: 'reseaux-bifurcation.json'
+ },
+ embeddings
+ }
+
+ fs.writeFileSync(outPath, JSON.stringify(output, null, 2), 'utf-8')
+
+ const sizeKb = Math.round(fs.statSync(outPath).size / 1024)
+ console.log()
+ console.log(`Done : ${embeddings.length} embeddings -> ${outPath}`)
+ console.log(`Taille : ${sizeKb} KB`)
+ console.log()
+ console.log('Prochaine etape : deployer le fichier sur le VPS avec les autres assets.')
+}
+
+main().catch(err => {
+ console.error('Erreur fatale :', err)
+ process.exit(1)
+})