mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
history_chatbot
This commit is contained in:
parent
5021251dbf
commit
ee79d56d66
@ -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,6 +140,19 @@ export default function ChatBot({ onClose }) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearHistory}
|
||||
disabled={messages.length === 0}
|
||||
aria-label="Effacer l'historique de conversation"
|
||||
title="Effacer l'historique"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full text-white transition-colors hover:bg-primary-container focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-fixed disabled:pointer-events-none disabled:opacity-40"
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl" aria-hidden="true" translate="no">
|
||||
delete_sweep
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@ -124,6 +168,7 @@ export default function ChatBot({ onClose }) {
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
@ -136,10 +181,11 @@ export default function ChatBot({ onClose }) {
|
||||
)}
|
||||
|
||||
{messages.map((msg, index) => {
|
||||
const msgKey = msg.id ?? `legacy-${index}`;
|
||||
if (msg.sender === "user") {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
key={msgKey}
|
||||
className="ml-auto max-w-[80%] rounded-sheet bg-primary px-3 py-2 font-headline text-xs leading-relaxed text-white"
|
||||
>
|
||||
{msg.text}
|
||||
@ -147,7 +193,7 @@ export default function ChatBot({ onClose }) {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={index} className="mr-auto flex max-w-[85%] flex-col gap-1.5">
|
||||
<div key={msgKey} className="mr-auto flex max-w-[85%] flex-col gap-1.5">
|
||||
<div
|
||||
className="rounded-sheet bg-surface-container px-3 py-2 text-on-surface [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
||||
>
|
||||
|
||||
86
app/utils/grasbotChatStorage.js
Normal file
86
app/utils/grasbotChatStorage.js
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Persistance locale du fil GrasBot (sans envoi à Ollama).
|
||||
*
|
||||
* Clé : `grasbot_chat_v1:<user_id>` 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();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user