feat(chatbot): persistance des conversations en sessionStorage + bouton Nouvelle conversation
- composables/useChatSession.ts : historique réactif partagé (useState) et persisté sous aep-chat-<clé>, version de schéma {v:1}, 40 derniers messages, restauration au mount uniquement (SSR-safe), try/catch partout, messages transient jamais écrits
- clés : accueil (Placeholder + Sheet), agences (Placeholder réseaux + ChatbotReseaux), taff, pensees (suffixée par l'auteur)
- ChatbotPlaceholder reçoit sa clé par prop chatKey (défaut accueil)
- ChatNewConversation : bouton sobre, visible seulement s'il y a un historique
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vcVGz8i4WpZm2dixKUiSD
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
31edbd37af
commit
75d8440601
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<button type="button" class="chat-new-conversation" @click="emit('reset')">
|
||||
Nouvelle conversation
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Bouton sobre « Nouvelle conversation », partagé par les chatbots.
|
||||
// Le parent le rend seulement s'il y a un historique et gère la purge.
|
||||
const emit = defineEmits<{ reset: [] }>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-new-conversation {
|
||||
align-self: flex-end;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--nav-font);
|
||||
color: var(--nav-text-muted);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-new-conversation:hover { color: var(--nav-text); background: var(--nav-bg-alt); }
|
||||
.chat-new-conversation:focus-visible { outline: 2px solid var(--nav-primary); outline-offset: 2px; }
|
||||
</style>
|
||||
@@ -16,16 +16,6 @@
|
||||
<span v-else class="chatbot-corpus-badge">FRACAS</span>
|
||||
</div>
|
||||
<div class="chatbot-header-right">
|
||||
<button
|
||||
v-if="messages.length > 0"
|
||||
class="chatbot-clear-btn"
|
||||
title="Effacer la conversation"
|
||||
@click="clearMessages"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
||||
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<select v-model="corpusMode" class="chatbot-corpus-select" title="Corpus interrogé">
|
||||
<option value="pensees">Pensées éco.</option>
|
||||
<option value="both">Pensées + Projets</option>
|
||||
@@ -66,6 +56,8 @@
|
||||
|
||||
<!-- Erreur -->
|
||||
<div v-if="errorMsg" class="chatbot-error">{{ errorMsg }}</div>
|
||||
|
||||
<ChatNewConversation v-if="hasHistory && !loading" @reset="clearMessages" />
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
@@ -109,20 +101,22 @@ const props = defineProps<{
|
||||
inline?: boolean
|
||||
}>()
|
||||
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
// Une conversation par contexte (corpus général ou un auteur) : changer d'auteur
|
||||
// ne mélange pas les historiques, et un rechargement restaure le bon.
|
||||
const sessionKey = computed(() => props.auteurContext ? `pensees-${slugify(props.auteurContext)}` : 'pensees')
|
||||
const { messages, hasHistory, clear } = useChatSession<ChatMessage>(sessionKey)
|
||||
const inputText = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const corpusMode = ref<'pensees' | 'projets' | 'both'>('pensees')
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
watch(() => props.auteurContext, () => {
|
||||
messages.value = []
|
||||
watch(sessionKey, () => {
|
||||
errorMsg.value = ''
|
||||
})
|
||||
|
||||
function clearMessages() {
|
||||
messages.value = []
|
||||
clear()
|
||||
errorMsg.value = ''
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
|
||||
<!-- Erreur -->
|
||||
<div v-if="errorMsg" class="error-bubble">{{ errorMsg }}</div>
|
||||
|
||||
<ChatNewConversation v-if="hasHistory && !loading" @reset="nouvelleConversation" />
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
@@ -169,7 +171,10 @@ const props = withDefaults(defineProps<{
|
||||
rendreMarkdown?: boolean
|
||||
/** Message affiché quand la limite quotidienne (429) est atteinte */
|
||||
messageLimite?: string
|
||||
/** Clé de persistance (sessionStorage). Même clé = même conversation (ex. mobile + desktop) */
|
||||
chatKey?: string
|
||||
}>(), {
|
||||
chatKey: 'accueil',
|
||||
endpoint: '/api/chatbot',
|
||||
titre: 'Chatbot AEP',
|
||||
placeholderReplie: 'Pose une question sur le réseau…',
|
||||
@@ -188,7 +193,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const { messages, hasHistory, clear } = useChatSession<ChatMessage>(() => props.chatKey)
|
||||
const inputText = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
@@ -225,6 +230,11 @@ function detectHashtagsFromQuery(query: string): string[] {
|
||||
return detected.slice(0, 3)
|
||||
}
|
||||
|
||||
function nouvelleConversation() {
|
||||
clear()
|
||||
errorMsg.value = ''
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
<span class="dot" /><span class="dot" /><span class="dot" />
|
||||
</div>
|
||||
<div v-if="errorMsg" class="error-bubble">{{ errorMsg }}</div>
|
||||
<ChatNewConversation v-if="hasHistory && !loading" @reset="nouvelleConversation" />
|
||||
</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));">
|
||||
@@ -117,7 +118,7 @@ interface ChatMessage { role: 'user' | 'assistant'; content: string; fiches?: Fi
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const { messages, hasHistory, clear } = useChatSession<ChatMessage>('agences')
|
||||
const inputText = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
@@ -136,6 +137,11 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function nouvelleConversation() {
|
||||
clear()
|
||||
errorMsg.value = ''
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const question = inputText.value.trim()
|
||||
if (!question || loading.value) return
|
||||
|
||||
@@ -114,6 +114,8 @@
|
||||
<div v-if="errorMsg" class="error-bubble">
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
|
||||
<ChatNewConversation v-if="hasHistory && !loading" @reset="nouvelleConversation" />
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
@@ -183,7 +185,7 @@ const emit = defineEmits<{
|
||||
'highlightOrgs': [ids: (number | string)[]]
|
||||
}>()
|
||||
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const { messages, hasHistory, clear } = useChatSession<ChatMessage>('accueil')
|
||||
const inputText = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
@@ -207,6 +209,11 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function nouvelleConversation() {
|
||||
clear()
|
||||
errorMsg.value = ''
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const question = inputText.value.trim()
|
||||
if (!question || loading.value) return
|
||||
|
||||
Reference in New Issue
Block a user