diff --git a/components/ChatNewConversation.vue b/components/ChatNewConversation.vue
new file mode 100644
index 0000000..fd2fb27
--- /dev/null
+++ b/components/ChatNewConversation.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/components/ChatbotPensees.vue b/components/ChatbotPensees.vue
index 110992e..7cec52b 100644
--- a/components/ChatbotPensees.vue
+++ b/components/ChatbotPensees.vue
@@ -16,16 +16,6 @@
FRACAS
@@ -109,20 +101,22 @@ const props = defineProps<{
inline?: boolean
}>()
-const messages = ref([])
+// 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(sessionKey)
const inputText = ref('')
const loading = ref(false)
const errorMsg = ref('')
const corpusMode = ref<'pensees' | 'projets' | 'both'>('pensees')
const messagesContainer = ref(null)
-watch(() => props.auteurContext, () => {
- messages.value = []
+watch(sessionKey, () => {
errorMsg.value = ''
})
function clearMessages() {
- messages.value = []
+ clear()
errorMsg.value = ''
}
diff --git a/components/ChatbotPlaceholder.vue b/components/ChatbotPlaceholder.vue
index 84a2f47..150347c 100644
--- a/components/ChatbotPlaceholder.vue
+++ b/components/ChatbotPlaceholder.vue
@@ -99,6 +99,8 @@
{{ errorMsg }}
+
+
@@ -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([])
+const { messages, hasHistory, clear } = useChatSession(() => 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
}
diff --git a/components/ChatbotReseaux.vue b/components/ChatbotReseaux.vue
index 9a5fb9a..5c3ffbe 100644
--- a/components/ChatbotReseaux.vue
+++ b/components/ChatbotReseaux.vue
@@ -74,6 +74,7 @@
{{ errorMsg }}
+
@@ -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
([])
+const { messages, hasHistory, clear } = useChatSession('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
diff --git a/components/ChatbotSheet.vue b/components/ChatbotSheet.vue
index 6556362..284a27b 100644
--- a/components/ChatbotSheet.vue
+++ b/components/ChatbotSheet.vue
@@ -114,6 +114,8 @@
{{ errorMsg }}
+
+
@@ -183,7 +185,7 @@ const emit = defineEmits<{
'highlightOrgs': [ids: (number | string)[]]
}>()
-const messages = ref([])
+const { messages, hasHistory, clear } = useChatSession('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
diff --git a/composables/useChatSession.ts b/composables/useChatSession.ts
new file mode 100644
index 0000000..2bfe76f
--- /dev/null
+++ b/composables/useChatSession.ts
@@ -0,0 +1,103 @@
+import type { MaybeRefOrGetter } from 'vue'
+
+/**
+ * Persistance de l'historique d'un chatbot dans sessionStorage.
+ *
+ * - Clé de stockage : `aep-chat-`, valeur `{ v: 1, messages: [...] }`
+ * (une valeur d'une autre version est ignorée).
+ * - SSR-safe : aucun accès au stockage hors de onMounted. Côté serveur et à
+ * l'hydratation l'historique est vide, il est restauré juste après le mount.
+ * - Partagé : deux composants (desktop + mobile) qui déclarent la même clé
+ * lisent et écrivent le même tableau (useState), donc un redimensionnement
+ * ne perd rien et un composant n'écrase pas l'autre.
+ * - Tolérant : toute lecture/écriture est dans un try/catch (navigation
+ * privée, stockage bloqué, quota). En cas d'échec le chatbot fonctionne
+ * comme avant, sans persistance.
+ * - Les messages marqués `transient: true` (erreurs) restent affichés mais ne
+ * sont jamais écrits. Chargement et erreurs vivent hors de l'historique.
+ * - La forme des messages est libre (fiches, auteur, mode...) : tout champ
+ * sérialisable est conservé.
+ */
+export interface ChatSessionMessage {
+ role: string
+ content: string
+ transient?: boolean
+}
+
+const STORAGE_PREFIX = 'aep-chat-'
+const SCHEMA_VERSION = 1
+const DEFAULT_MAX = 40
+
+export function useChatSession(
+ pageKey: MaybeRefOrGetter,
+ options: { max?: number } = {},
+) {
+ const max = options.max ?? DEFAULT_MAX
+ const store = useState>('aep-chat-store', () => ({}))
+ const restored = useState>('aep-chat-restored', () => ({}))
+ const key = computed(() => toValue(pageKey))
+ const mounted = ref(false)
+
+ function ensure(k: string) {
+ if (!store.value[k]) store.value[k] = []
+ }
+
+ const messages = computed({
+ get: () => store.value[key.value] ?? [],
+ set: (list) => { store.value[key.value] = list },
+ })
+ const hasHistory = computed(() => messages.value.length > 0)
+
+ function isValid(m: any): m is T {
+ return !!m && typeof m === 'object' && typeof m.role === 'string' && typeof m.content === 'string'
+ }
+
+ function restore() {
+ const k = key.value
+ if (restored.value[k]) return
+ restored.value[k] = true
+ ensure(k)
+ try {
+ const raw = sessionStorage.getItem(STORAGE_PREFIX + k)
+ if (!raw) return
+ const data = JSON.parse(raw)
+ if (!data || data.v !== SCHEMA_VERSION || !Array.isArray(data.messages)) return
+ const valid = (data.messages as unknown[]).filter(isValid).slice(-max)
+ if (valid.length && store.value[k].length === 0) store.value[k] = valid
+ } catch {
+ /* stockage indisponible ou valeur corrompue : on repart de zéro */
+ }
+ }
+
+ function save() {
+ if (!mounted.value) return
+ try {
+ const list = messages.value.filter(m => !m.transient).slice(-max)
+ const sk = STORAGE_PREFIX + key.value
+ if (list.length === 0) sessionStorage.removeItem(sk)
+ else sessionStorage.setItem(sk, JSON.stringify({ v: SCHEMA_VERSION, messages: list }))
+ } catch {
+ /* quota, navigation privée : pas de persistance, pas d'erreur */
+ }
+ }
+
+ /** Efface l'historique affiché et la clé de stockage. */
+ function clear() {
+ store.value[key.value] = []
+ try { sessionStorage.removeItem(STORAGE_PREFIX + key.value) } catch { /* ignoré */ }
+ }
+
+ watch(key, (k) => {
+ ensure(k)
+ if (mounted.value) restore()
+ }, { immediate: true })
+
+ watch(messages, save, { deep: true })
+
+ onMounted(() => {
+ restore()
+ mounted.value = true
+ })
+
+ return { messages, hasHistory, clear }
+}
diff --git a/pages/agences.vue b/pages/agences.vue
index f5112fc..3893571 100644
--- a/pages/agences.vue
+++ b/pages/agences.vue
@@ -59,6 +59,7 @@
+
+
@@ -581,6 +583,8 @@ interface ChatMessage {
role: 'user' | 'bot'
content: string
recommandations?: { id: string; nom: string; raison: string }[]
+ /** Message d'erreur : affiché mais jamais persisté */
+ transient?: boolean
}
const { render: renderMd } = useMarkdown()
@@ -588,7 +592,7 @@ const { render: renderMd } = useMarkdown()
const chatOpen = ref(false)
const chatInput = ref('')
const chatLoading = ref(false)
-const chatMessages = ref([])
+const { messages: chatMessages, hasHistory: chatHasHistory, clear: clearChat } = useChatSession('taff')
const chatMessagesEl = ref(null)
async function sendChat() {
@@ -610,7 +614,7 @@ async function sendChat() {
recommandations: res.plateformes_recommandees ?? [],
})
} catch (e: any) {
- chatMessages.value.push({ role: 'bot', content: e?.data?.statusMessage ?? 'Erreur — réessaie dans un instant.' })
+ chatMessages.value.push({ role: 'bot', content: e?.data?.statusMessage ?? 'Erreur — réessaie dans un instant.', transient: true })
} finally {
chatLoading.value = false
await nextTick()