refactor(chatbot): ChatbotPlaceholder paramétrable, /agences desktop sur chatbot-reseaux

- ChatbotPlaceholder : props endpoint, titre, placeholderReplie, placeholderSaisie, intro, exemple, fichesTitre, ficheLienModele, rendreMarkdown, messageLimite ; défauts = comportement actuel de l'accueil
- pages/agences.vue : le desktop appelle /api/chatbot-reseaux (le câblage croisé vers le backend Entraide est supprimé), textes orientés structures, rendu markdown, liens /agences#id
- suppression de server/api/chatbot-v2.post.ts et server/utils/vectorSearch.ts (aucun appelant) ; base recherche sémantique par embeddings retrouvable au commit 39602e26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vcVGz8i4WpZm2dixKUiSD
This commit is contained in:
Jules Neny
2026-10-09 03:32:18 +02:00
co-authored by Claude Opus 5.5
parent 39602e26c7
commit 31edbd37af
4 changed files with 62 additions and 300 deletions
+52 -10
View File
@@ -12,7 +12,7 @@
role="button"
tabindex="0"
:aria-expanded="isExpanded"
aria-label="Ouvrir l'assistant chatbot"
:aria-label="`Ouvrir l'assistant : ${titre}`"
@keydown.enter="toggleExpand"
@keydown.space.prevent="toggleExpand"
>
@@ -27,7 +27,7 @@
</div>
<span class="flex-1 text-sm" style="color: var(--nav-text-muted);">
{{ isExpanded ? 'Chatbot AEP' : 'Pose une question sur le réseau…' }}
{{ isExpanded ? titre : placeholderReplie }}
</span>
<!-- Chevron -->
@@ -52,21 +52,22 @@
<div class="chatbot-body-inner" ref="messagesContainer">
<!-- Onboarding -->
<div v-if="messages.length === 0" class="onboarding-bubble">
<p>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.</p>
<p class="example">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."</p>
<p>{{ intro }}</p>
<p v-if="exemple" class="example">{{ exemple }}</p>
</div>
<!-- Messages -->
<template v-for="(msg, i) in messages" :key="i">
<div v-if="msg.role === 'user'" class="user-bubble">{{ msg.content }}</div>
<div v-else class="assistant-bubble">
<p>{{ msg.content }}</p>
<div v-if="rendreMarkdown" v-html="renderMd(msg.content)" />
<p v-else>{{ msg.content }}</p>
<div v-if="msg.fiches && msg.fiches.length > 0" class="fiches-list">
<p class="fiches-title">Fiches recommandees :</p>
<p class="fiches-title">{{ fichesTitre }}</p>
<a
v-for="fiche in msg.fiches"
:key="fiche.id"
:href="`/fiche/${fiche.id}`"
:href="ficheLienModele.replace('{id}', String(fiche.id))"
class="fiche-card"
>
<span class="fiche-nom">{{ fiche.nom }}</span>
@@ -106,7 +107,7 @@
v-model="inputText"
type="text"
:disabled="loading"
placeholder="Pose ta question…"
:placeholder="placeholderSaisie"
class="chatbot-input"
@keydown.enter.prevent="sendMessage"
/>
@@ -127,6 +128,9 @@
</template>
<script setup lang="ts">
import { useMarkdown } from '~/composables/useMarkdown'
const { render: renderMd } = useMarkdown()
interface FicheReco {
id: number | string
nom: string
@@ -140,6 +144,44 @@ interface ChatMessage {
suggestedHashtags?: string[]
}
// Composant unique pour les deux chatbots desktop. Les défauts reproduisent
// exactement le comportement de l'accueil (/api/chatbot, backend Entraide).
// Les deux backends renvoient { reponse_texte, fiches_recommandees } ; seul
// le rendu diffère (texte brut vs markdown, lien de fiche).
const props = withDefaults(defineProps<{
/** Route serveur appelée (body { question }) */
endpoint?: string
/** Titre affiché dans l'en-tête déplié */
titre?: string
/** Texte de l'en-tête replié */
placeholderReplie?: string
/** Placeholder du champ de saisie */
placeholderSaisie?: string
/** Texte d'accueil (première bulle) */
intro?: string
/** Exemple de question, affiché sous l'accueil */
exemple?: string
/** Intitulé de la liste de fiches/structures citées */
fichesTitre?: string
/** Modèle de lien vers une fiche, {id} est remplacé */
ficheLienModele?: string
/** Rendre la réponse en markdown (backend réseaux) plutôt qu'en texte brut */
rendreMarkdown?: boolean
/** Message affiché quand la limite quotidienne (429) est atteinte */
messageLimite?: string
}>(), {
endpoint: '/api/chatbot',
titre: 'Chatbot AEP',
placeholderReplie: 'Pose une question sur le réseau…',
placeholderSaisie: 'Pose ta question…',
intro: "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: '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."',
fichesTitre: 'Fiches recommandees :',
ficheLienModele: '/fiche/{id}',
rendreMarkdown: false,
messageLimite: 'Limite de 10 questions par jour atteinte. Reviens demain.',
})
const emit = defineEmits<{
'highlightOrgs': [ids: (number | string)[]]
'applyHashtag': [tag: string]
@@ -203,7 +245,7 @@ async function sendMessage() {
const res = await $fetch<{
reponse_texte: string
fiches_recommandees: { id: number | string; nom: string; explication: string }[]
}>('/api/chatbot', {
}>(props.endpoint, {
method: 'POST',
body: { question },
})
@@ -223,7 +265,7 @@ async function sendMessage() {
} catch (e: any) {
const status = e?.statusCode ?? e?.status
if (status === 429) {
errorMsg.value = 'Limite de 10 questions par jour atteinte. Reviens demain.'
errorMsg.value = props.messageLimite
} else if (status === 503) {
errorMsg.value = 'Le budget IA mensuel est épuisé. Soutiens NAV sur Liberapay pour continuer.'
} else {