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 } }