From ee79d56d66f1b3a90c66f957f488e122cb336edd Mon Sep 17 00:00:00 2001 From: Ladebeze66 Date: Fri, 1 May 2026 12:30:39 +0200 Subject: [PATCH] history_chatbot --- app/components/ChatBot.js | 80 +++++++++++++++++++++++------- app/utils/grasbotChatStorage.js | 86 +++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 app/utils/grasbotChatStorage.js diff --git a/app/components/ChatBot.js b/app/components/ChatBot.js index 869ec3f..a452cb7 100644 --- a/app/components/ChatBot.js +++ b/app/components/ChatBot.js @@ -1,10 +1,16 @@ "use client"; import Link from "next/link"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { askAI } from "../utils/askAI"; +import { + clearGrasbotChatMessages, + loadGrasbotChatMessages, + newChatMessageId, + saveGrasbotChatMessages, +} from "../utils/grasbotChatStorage"; /** * GrasBot — UI du chatbot (Stitch). @@ -25,6 +31,12 @@ import { askAI } from "../utils/askAI"; * listes, liens cliquables. Texte justifié dans la bulle. Les messages * utilisateur restent en texte brut. * + * v3.3 (2026-04-26) : + * - Historique persisté en **localStorage** par `grasbot_user_id` (voir + * `grasbotChatStorage.js`) : même navigateur / effacement cookies selon usage, + * jusqu'à 80 messages récents. Aucune réinjection dans Ollama. + * - Bouton pour effacer la conversation locale. + * * Design : * - Fond `surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient`. * - Bulles : user = `bg-primary text-white` à droite, bot = `bg-surface-container` à gauche. @@ -34,10 +46,27 @@ import { askAI } from "../utils/askAI"; export default function ChatBot({ onClose }) { const [question, setQuestion] = useState(""); const [messages, setMessages] = useState([]); + const [hydrated, setHydrated] = useState(false); const [isWaiting, setIsWaiting] = useState(false); const scrollRef = useRef(null); const inputRef = useRef(null); + useLayoutEffect(() => { + setMessages(loadGrasbotChatMessages()); + setHydrated(true); + }, []); + + useEffect(() => { + if (!hydrated) return; + saveGrasbotChatMessages(messages); + }, [messages, hydrated]); + + const handleClearHistory = () => { + setMessages([]); + clearGrasbotChatMessages(); + inputRef.current?.focus(); + }; + useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; @@ -51,7 +80,7 @@ export default function ChatBot({ onClose }) { const handleAsk = async () => { if (!question.trim() || isWaiting) return; - const userMessage = { sender: "user", text: question }; + const userMessage = { sender: "user", text: question, id: newChatMessageId() }; setMessages((prev) => [...prev, userMessage]); setQuestion(""); setIsWaiting(true); @@ -66,6 +95,7 @@ export default function ChatBot({ onClose }) { sources: payload.sources || [], grounded: Boolean(payload.grounded), timeout: Boolean(payload._timeout), + id: newChatMessageId(), }, ]); } catch (_error) { @@ -77,6 +107,7 @@ export default function ChatBot({ onClose }) { sources: [], grounded: false, error: true, + id: newChatMessageId(), }, ]); } finally { @@ -109,20 +140,34 @@ export default function ChatBot({ onClose }) {

- + + + +
{ + const msgKey = msg.id ?? `legacy-${index}`; if (msg.sender === "user") { return (
{msg.text} @@ -147,7 +193,7 @@ export default function ChatBot({ onClose }) { ); } return ( -
+
diff --git a/app/utils/grasbotChatStorage.js b/app/utils/grasbotChatStorage.js new file mode 100644 index 0000000..0f0558f --- /dev/null +++ b/app/utils/grasbotChatStorage.js @@ -0,0 +1,86 @@ +/** + * Persistance locale du fil GrasBot (sans envoi à Ollama). + * + * Clé : `grasbot_chat_v1:` où `user_id` est celui de `grasbotIds.js`. + * Limite : les derniers messages uniquement pour éviter quota localStorage (~5 Mo). + */ + +import { getGrasbotUserId } from "./grasbotIds"; + +export const GRASBOT_CHAT_STORAGE_VERSION = 1; +export const GRASBOT_CHAT_MAX_MESSAGES = 80; + +function storageKey(userId) { + return `grasbot_chat_v${GRASBOT_CHAT_STORAGE_VERSION}:${userId}`; +} + +function safeRandomId() { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return `m_${Date.now()}_${Math.random().toString(16).slice(2)}`; +} + +/** + * @param {unknown} m + * @returns {object | null} + */ +function sanitizeMessage(m) { + if (!m || typeof m !== "object") return null; + if (m.sender !== "user" && m.sender !== "bot") return null; + if (typeof m.text !== "string") return null; + const out = { sender: m.sender, text: m.text }; + if (typeof m.id === "string") out.id = m.id; + if (m.sender === "bot") { + if (Array.isArray(m.sources)) out.sources = m.sources; + if (typeof m.grounded === "boolean") out.grounded = m.grounded; + if (typeof m.timeout === "boolean") out.timeout = m.timeout; + if (typeof m.error === "boolean") out.error = m.error; + } + return out; +} + +/** @returns {object[]} */ +export function loadGrasbotChatMessages() { + if (typeof window === "undefined") return []; + try { + const uid = getGrasbotUserId(); + if (!uid) return []; + const raw = window.localStorage.getItem(storageKey(uid)); + if (!raw) return []; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return []; + return parsed.map(sanitizeMessage).filter(Boolean); + } catch { + return []; + } +} + +/** @param {object[]} messages */ +export function saveGrasbotChatMessages(messages) { + if (typeof window === "undefined") return; + try { + const uid = getGrasbotUserId(); + if (!uid) return; + const trimmed = messages.slice(-GRASBOT_CHAT_MAX_MESSAGES); + const serialized = trimmed.map(sanitizeMessage).filter(Boolean); + window.localStorage.setItem(storageKey(uid), JSON.stringify(serialized)); + } catch { + // quota / mode privé strict + } +} + +export function clearGrasbotChatMessages() { + if (typeof window === "undefined") return; + try { + const uid = getGrasbotUserId(); + if (!uid) return; + window.localStorage.removeItem(storageKey(uid)); + } catch { + /* ignore */ + } +} + +export function newChatMessageId() { + return safeRandomId(); +}