feat(chatbot): toggle de mode branché sur les 5 interfaces chatbot

Placeholder (/ et /agences desktop), Sheet (/ mobile), Réseaux (/agences mobile),
chatbot de /trouver-du-taf et Pensées (/media) envoient `profondeur` dans le body
et l'enregistrent sur chaque message (badge conservé à la restauration).

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:49:56 +02:00
co-authored by Claude Opus 5.5
parent 4c57f30f6a
commit 52b79e1e8e
5 changed files with 70 additions and 9 deletions
+22
View File
@@ -42,6 +42,7 @@
<template v-for="(msg, i) in messages" :key="i"> <template v-for="(msg, i) in messages" :key="i">
<div v-if="msg.role === 'user'" class="chatbot-bubble user">{{ msg.content }}</div> <div v-if="msg.role === 'user'" class="chatbot-bubble user">{{ msg.content }}</div>
<div v-else class="chatbot-bubble assistant"> <div v-else class="chatbot-bubble assistant">
<ChatModeBadge :profondeur="msg.profondeur" />
<div class="chatbot-md" v-html="renderMd(msg.content)" /> <div class="chatbot-md" v-html="renderMd(msg.content)" />
<div v-if="msg.auteur" class="chatbot-auteur-tag"> <div v-if="msg.auteur" class="chatbot-auteur-tag">
<span>{{ msg.auteur.nom }}</span> <span>{{ msg.auteur.nom }}</span>
@@ -60,6 +61,11 @@
<ChatNewConversation v-if="hasHistory && !loading" @reset="clearMessages" /> <ChatNewConversation v-if="hasHistory && !loading" @reset="clearMessages" />
</div> </div>
<!-- Mode de réponse -->
<div class="chatbot-mode-row">
<ChatModeToggle v-model="profondeur" :disabled="loading" />
</div>
<!-- Input --> <!-- Input -->
<div class="chatbot-input-row"> <div class="chatbot-input-row">
<input <input
@@ -94,6 +100,7 @@ interface ChatMessage {
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: string content: string
auteur?: { slug: string; nom: string } | null auteur?: { slug: string; nom: string } | null
profondeur?: string
} }
const props = defineProps<{ const props = defineProps<{
@@ -105,6 +112,8 @@ const props = defineProps<{
// ne mélange pas les historiques, et un rechargement restaure le bon. // ne mélange pas les historiques, et un rechargement restaure le bon.
const sessionKey = computed(() => props.auteurContext ? `pensees-${slugify(props.auteurContext)}` : 'pensees') const sessionKey = computed(() => props.auteurContext ? `pensees-${slugify(props.auteurContext)}` : 'pensees')
const { messages, hasHistory, clear } = useChatSession<ChatMessage>(sessionKey) const { messages, hasHistory, clear } = useChatSession<ChatMessage>(sessionKey)
// `profondeur` (et non `mode`, réservé au mode de recherche LightRAG côté serveur)
const profondeur = useChatProfondeur('pensees')
const inputText = ref('') const inputText = ref('')
const loading = ref(false) const loading = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
@@ -134,6 +143,7 @@ async function sendMessage() {
errorMsg.value = '' errorMsg.value = ''
messages.value.push({ role: 'user', content: query }) messages.value.push({ role: 'user', content: query })
loading.value = true loading.value = true
const mode = profondeur.value
await nextTick() await nextTick()
scrollToBottom() scrollToBottom()
@@ -147,6 +157,7 @@ async function sendMessage() {
body: { body: {
query, query,
corpus: corpusMode.value, corpus: corpusMode.value,
profondeur: mode,
auteur_slug: props.auteurContext ? slugify(props.auteurContext) : undefined, auteur_slug: props.auteurContext ? slugify(props.auteurContext) : undefined,
}, },
}) })
@@ -154,6 +165,7 @@ async function sendMessage() {
role: 'assistant', role: 'assistant',
content: res.response, content: res.response,
auteur: res.auteur ?? null, auteur: res.auteur ?? null,
profondeur: mode,
}) })
} catch (e: any) { } catch (e: any) {
const status = e?.statusCode ?? e?.status const status = e?.statusCode ?? e?.status
@@ -382,6 +394,16 @@ function slugify(str: string): string {
max-width: 90%; max-width: 90%;
} }
/* Mode de réponse */
.chatbot-mode-row {
flex-shrink: 0;
display: flex;
padding: 6px 10px 0;
border-top: 1px solid rgba(180, 170, 160, 0.22);
background: var(--nav-surface);
}
.chatbot-mode-row + .chatbot-input-row { border-top: none; }
/* Input */ /* Input */
.chatbot-input-row { .chatbot-input-row {
flex-shrink: 0; flex-shrink: 0;
+17 -2
View File
@@ -60,6 +60,7 @@
<template v-for="(msg, i) in messages" :key="i"> <template v-for="(msg, i) in messages" :key="i">
<div v-if="msg.role === 'user'" class="user-bubble">{{ msg.content }}</div> <div v-if="msg.role === 'user'" class="user-bubble">{{ msg.content }}</div>
<div v-else class="assistant-bubble"> <div v-else class="assistant-bubble">
<ChatModeBadge :profondeur="msg.profondeur" />
<div v-if="rendreMarkdown" v-html="renderMd(msg.content)" /> <div v-if="rendreMarkdown" v-html="renderMd(msg.content)" />
<p v-else>{{ msg.content }}</p> <p v-else>{{ msg.content }}</p>
<div v-if="msg.fiches && msg.fiches.length > 0" class="fiches-list"> <div v-if="msg.fiches && msg.fiches.length > 0" class="fiches-list">
@@ -103,8 +104,13 @@
<ChatNewConversation v-if="hasHistory && !loading" @reset="nouvelleConversation" /> <ChatNewConversation v-if="hasHistory && !loading" @reset="nouvelleConversation" />
</div> </div>
<!-- Mode de réponse -->
<div class="chatbot-mode-row" style="border-top: 1px solid var(--nav-bg-alt);">
<ChatModeToggle v-model="profondeur" :disabled="loading" />
</div>
<!-- Input --> <!-- Input -->
<div class="chatbot-input-row" style="border-top: 1px solid var(--nav-bg-alt);"> <div class="chatbot-input-row">
<input <input
v-model="inputText" v-model="inputText"
type="text" type="text"
@@ -144,6 +150,7 @@ interface ChatMessage {
content: string content: string
fiches?: FicheReco[] fiches?: FicheReco[]
suggestedHashtags?: string[] suggestedHashtags?: string[]
profondeur?: string
} }
// Composant unique pour les deux chatbots desktop. Les défauts reproduisent // Composant unique pour les deux chatbots desktop. Les défauts reproduisent
@@ -194,6 +201,7 @@ const emit = defineEmits<{
const isExpanded = ref(false) const isExpanded = ref(false)
const { messages, hasHistory, clear } = useChatSession<ChatMessage>(() => props.chatKey) const { messages, hasHistory, clear } = useChatSession<ChatMessage>(() => props.chatKey)
const profondeur = useChatProfondeur(() => props.chatKey)
const inputText = ref('') const inputText = ref('')
const loading = ref(false) const loading = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
@@ -247,6 +255,7 @@ async function sendMessage() {
errorMsg.value = '' errorMsg.value = ''
messages.value.push({ role: 'user', content: question }) messages.value.push({ role: 'user', content: question })
loading.value = true loading.value = true
const mode = profondeur.value
await nextTick() await nextTick()
scrollToBottom() scrollToBottom()
@@ -257,7 +266,7 @@ async function sendMessage() {
fiches_recommandees: { id: number | string; nom: string; explication: string }[] fiches_recommandees: { id: number | string; nom: string; explication: string }[]
}>(props.endpoint, { }>(props.endpoint, {
method: 'POST', method: 'POST',
body: { question }, body: { question, profondeur: mode },
}) })
const suggestedHashtags = detectHashtagsFromQuery(question) const suggestedHashtags = detectHashtagsFromQuery(question)
@@ -266,6 +275,7 @@ async function sendMessage() {
content: res.reponse_texte, content: res.reponse_texte,
fiches: res.fiches_recommandees || [], fiches: res.fiches_recommandees || [],
suggestedHashtags: suggestedHashtags.length ? suggestedHashtags : undefined, suggestedHashtags: suggestedHashtags.length ? suggestedHashtags : undefined,
profondeur: mode,
} }
messages.value.push(assistantMsg) messages.value.push(assistantMsg)
@@ -357,6 +367,11 @@ function scrollToBottom() {
gap: 8px; gap: 8px;
} }
.chatbot-mode-row {
display: flex;
padding: 8px 12px 0;
flex-shrink: 0;
}
.chatbot-input-row { .chatbot-input-row {
display: flex; display: flex;
align-items: center; align-items: center;
+7 -3
View File
@@ -54,6 +54,7 @@
<template v-for="(msg, i) in messages" :key="i"> <template v-for="(msg, i) in messages" :key="i">
<div v-if="msg.role === 'user'" class="user-bubble">{{ msg.content }}</div> <div v-if="msg.role === 'user'" class="user-bubble">{{ msg.content }}</div>
<div v-else class="assistant-bubble"> <div v-else class="assistant-bubble">
<ChatModeBadge :profondeur="msg.profondeur" />
<div v-html="renderMd(msg.content)" /> <div v-html="renderMd(msg.content)" />
<div v-if="msg.fiches && msg.fiches.length > 0" class="fiches-list" style="margin-top:12px;"> <div v-if="msg.fiches && msg.fiches.length > 0" class="fiches-list" style="margin-top:12px;">
<p class="fiches-title">Structures recommandées :</p> <p class="fiches-title">Structures recommandées :</p>
@@ -78,6 +79,7 @@
</div> </div>
<div class="shrink-0 px-4 pt-3 border-t" style="border-color: var(--nav-bg-alt); padding-bottom: max(1rem, env(safe-area-inset-bottom));"> <div class="shrink-0 px-4 pt-3 border-t" style="border-color: var(--nav-bg-alt); padding-bottom: max(1rem, env(safe-area-inset-bottom));">
<ChatModeToggle v-model="profondeur" :disabled="loading" class="mb-2" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <input
v-model="inputText" v-model="inputText"
@@ -113,12 +115,13 @@ import { useMarkdown } from '~/composables/useMarkdown'
const { render: renderMd } = useMarkdown() const { render: renderMd } = useMarkdown()
interface FicheReco { id: number | string; nom: string; explication?: string } interface FicheReco { id: number | string; nom: string; explication?: string }
interface ChatMessage { role: 'user' | 'assistant'; content: string; fiches?: FicheReco[] } interface ChatMessage { role: 'user' | 'assistant'; content: string; fiches?: FicheReco[]; profondeur?: string }
const props = defineProps<{ modelValue: boolean }>() const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>() const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const { messages, hasHistory, clear } = useChatSession<ChatMessage>('agences') const { messages, hasHistory, clear } = useChatSession<ChatMessage>('agences')
const profondeur = useChatProfondeur('agences')
const inputText = ref('') const inputText = ref('')
const loading = ref(false) const loading = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
@@ -153,11 +156,12 @@ async function sendMessage() {
if (messagesContainer.value) messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight if (messagesContainer.value) messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
try { try {
const mode = profondeur.value
const res = await $fetch<{ reponse_texte: string; fiches_recommandees: FicheReco[] }>( const res = await $fetch<{ reponse_texte: string; fiches_recommandees: FicheReco[] }>(
'/api/chatbot-reseaux', '/api/chatbot-reseaux',
{ method: 'POST', body: { question } } { method: 'POST', body: { question, profondeur: mode } }
) )
messages.value.push({ role: 'assistant', content: res.reponse_texte, fiches: res.fiches_recommandees || [] }) messages.value.push({ role: 'assistant', content: res.reponse_texte, fiches: res.fiches_recommandees || [], profondeur: mode })
} catch (e: any) { } catch (e: any) {
const s = e?.statusCode ?? e?.status const s = e?.statusCode ?? e?.status
errorMsg.value = s === 429 errorMsg.value = s === 429
+7 -1
View File
@@ -87,6 +87,7 @@
<!-- Message assistant --> <!-- Message assistant -->
<div v-else class="assistant-bubble"> <div v-else class="assistant-bubble">
<ChatModeBadge :profondeur="msg.profondeur" />
<div class="md-content" v-html="renderMd(msg.content)" /> <div class="md-content" v-html="renderMd(msg.content)" />
<!-- Fiches recommandées --> <!-- Fiches recommandées -->
@@ -123,6 +124,7 @@
class="shrink-0 px-4 pt-3 border-t" class="shrink-0 px-4 pt-3 border-t"
style="border-color: var(--nav-bg-alt); padding-bottom: max(1rem, env(safe-area-inset-bottom));" style="border-color: var(--nav-bg-alt); padding-bottom: max(1rem, env(safe-area-inset-bottom));"
> >
<ChatModeToggle v-model="profondeur" :disabled="loading" class="mb-2" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <input
v-model="inputText" v-model="inputText"
@@ -174,6 +176,7 @@ interface ChatMessage {
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: string content: string
fiches?: FicheReco[] fiches?: FicheReco[]
profondeur?: string
} }
const props = defineProps<{ const props = defineProps<{
@@ -186,6 +189,7 @@ const emit = defineEmits<{
}>() }>()
const { messages, hasHistory, clear } = useChatSession<ChatMessage>('accueil') const { messages, hasHistory, clear } = useChatSession<ChatMessage>('accueil')
const profondeur = useChatProfondeur('accueil')
const inputText = ref('') const inputText = ref('')
const loading = ref(false) const loading = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
@@ -222,6 +226,7 @@ async function sendMessage() {
errorMsg.value = '' errorMsg.value = ''
messages.value.push({ role: 'user', content: question }) messages.value.push({ role: 'user', content: question })
loading.value = true loading.value = true
const mode = profondeur.value
await nextTick() await nextTick()
scrollToBottom() scrollToBottom()
@@ -232,13 +237,14 @@ async function sendMessage() {
fiches_recommandees: { id: number | string; nom: string; explication: string }[] fiches_recommandees: { id: number | string; nom: string; explication: string }[]
}>('/api/chatbot', { }>('/api/chatbot', {
method: 'POST', method: 'POST',
body: { question }, body: { question, profondeur: mode },
}) })
const assistantMsg: ChatMessage = { const assistantMsg: ChatMessage = {
role: 'assistant', role: 'assistant',
content: res.reponse_texte, content: res.reponse_texte,
fiches: res.fiches_recommandees || [], fiches: res.fiches_recommandees || [],
profondeur: mode,
} }
messages.value.push(assistantMsg) messages.value.push(assistantMsg)
+17 -3
View File
@@ -254,8 +254,11 @@
<!-- Messages de la conversation --> <!-- Messages de la conversation -->
<template v-for="(msg, i) in chatMessages" :key="i"> <template v-for="(msg, i) in chatMessages" :key="i">
<div :class="['taff-msg', msg.role === 'user' ? 'taff-msg--user' : 'taff-msg--bot']"> <div :class="['taff-msg', msg.role === 'user' ? 'taff-msg--user' : 'taff-msg--bot']">
<div v-if="msg.role === 'bot'" class="md-content" v-html="renderMd(msg.content)" /> <template v-if="msg.role === 'bot'">
<span v-else>{{ msg.content }}</span> <ChatModeBadge :profondeur="msg.profondeur" />
<div class="md-content" v-html="renderMd(msg.content)" />
</template>
<span v-if="msg.role !== 'bot'">{{ msg.content }}</span>
</div> </div>
<!-- Plateformes recommandées --> <!-- Plateformes recommandées -->
<div v-if="msg.role === 'bot' && msg.recommandations?.length" class="taff-chat-recos"> <div v-if="msg.role === 'bot' && msg.recommandations?.length" class="taff-chat-recos">
@@ -279,6 +282,11 @@
<ChatNewConversation v-if="chatHasHistory && !chatLoading" @reset="clearChat" /> <ChatNewConversation v-if="chatHasHistory && !chatLoading" @reset="clearChat" />
</div> </div>
<!-- Mode de réponse -->
<div class="taff-chat-mode">
<ChatModeToggle v-model="chatProfondeur" :disabled="chatLoading" />
</div>
<!-- Input --> <!-- Input -->
<div class="taff-chat-input-wrap"> <div class="taff-chat-input-wrap">
<textarea <textarea
@@ -583,6 +591,7 @@ interface ChatMessage {
role: 'user' | 'bot' role: 'user' | 'bot'
content: string content: string
recommandations?: { id: string; nom: string; raison: string }[] recommandations?: { id: string; nom: string; raison: string }[]
profondeur?: string
/** Message d'erreur : affiché mais jamais persisté */ /** Message d'erreur : affiché mais jamais persisté */
transient?: boolean transient?: boolean
} }
@@ -593,6 +602,7 @@ const chatOpen = ref(false)
const chatInput = ref('') const chatInput = ref('')
const chatLoading = ref(false) const chatLoading = ref(false)
const { messages: chatMessages, hasHistory: chatHasHistory, clear: clearChat } = useChatSession<ChatMessage>('taff') const { messages: chatMessages, hasHistory: chatHasHistory, clear: clearChat } = useChatSession<ChatMessage>('taff')
const chatProfondeur = useChatProfondeur('taff')
const chatMessagesEl = ref<HTMLElement | null>(null) const chatMessagesEl = ref<HTMLElement | null>(null)
async function sendChat() { async function sendChat() {
@@ -601,17 +611,19 @@ async function sendChat() {
chatMessages.value.push({ role: 'user', content: q }) chatMessages.value.push({ role: 'user', content: q })
chatInput.value = '' chatInput.value = ''
chatLoading.value = true chatLoading.value = true
const mode = chatProfondeur.value
await nextTick() await nextTick()
chatMessagesEl.value?.scrollTo({ top: chatMessagesEl.value.scrollHeight, behavior: 'smooth' }) chatMessagesEl.value?.scrollTo({ top: chatMessagesEl.value.scrollHeight, behavior: 'smooth' })
try { try {
const res = await $fetch<{ reponse_texte: string; plateformes_recommandees: { id: string; nom: string; raison: string }[] }>( const res = await $fetch<{ reponse_texte: string; plateformes_recommandees: { id: string; nom: string; raison: string }[] }>(
'/api/chatbot-taff', '/api/chatbot-taff',
{ method: 'POST', body: { question: q } } { method: 'POST', body: { question: q, profondeur: mode } }
) )
chatMessages.value.push({ chatMessages.value.push({
role: 'bot', role: 'bot',
content: res.reponse_texte, content: res.reponse_texte,
recommandations: res.plateformes_recommandees ?? [], recommandations: res.plateformes_recommandees ?? [],
profondeur: mode,
}) })
} catch (e: any) { } catch (e: any) {
chatMessages.value.push({ role: 'bot', content: e?.data?.statusMessage ?? 'Erreur — réessaie dans un instant.', transient: true }) chatMessages.value.push({ role: 'bot', content: e?.data?.statusMessage ?? 'Erreur — réessaie dans un instant.', transient: true })
@@ -972,6 +984,8 @@ const parsedDescription = computed(() => {
40% { transform: translateY(-5px); opacity: 1; } 40% { transform: translateY(-5px); opacity: 1; }
} }
.taff-chat-mode { padding: 0.5rem 1rem 0; }
.taff-chat-input-wrap { .taff-chat-input-wrap {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;