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 }) {
-
+
+ delete_sweep
+
+
+
+
+ close
+
+
+
{
+ 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();
+}