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";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { askAI } from "../utils/askAI";
|
import { askAI } from "../utils/askAI";
|
||||||
|
import {
|
||||||
|
clearGrasbotChatMessages,
|
||||||
|
loadGrasbotChatMessages,
|
||||||
|
newChatMessageId,
|
||||||
|
saveGrasbotChatMessages,
|
||||||
|
} from "../utils/grasbotChatStorage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GrasBot — UI du chatbot (Stitch).
|
* GrasBot — UI du chatbot (Stitch).
|
||||||
@ -25,6 +31,12 @@ import { askAI } from "../utils/askAI";
|
|||||||
* listes, liens cliquables. Texte justifié dans la bulle. Les messages
|
* listes, liens cliquables. Texte justifié dans la bulle. Les messages
|
||||||
* utilisateur restent en texte brut.
|
* 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 :
|
* Design :
|
||||||
* - Fond `surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient`.
|
* - Fond `surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient`.
|
||||||
* - Bulles : user = `bg-primary text-white` à droite, bot = `bg-surface-container` à gauche.
|
* - 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 }) {
|
export default function ChatBot({ onClose }) {
|
||||||
const [question, setQuestion] = useState("");
|
const [question, setQuestion] = useState("");
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [hydrated, setHydrated] = useState(false);
|
||||||
const [isWaiting, setIsWaiting] = useState(false);
|
const [isWaiting, setIsWaiting] = useState(false);
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
const inputRef = 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(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
@ -51,7 +80,7 @@ export default function ChatBot({ onClose }) {
|
|||||||
const handleAsk = async () => {
|
const handleAsk = async () => {
|
||||||
if (!question.trim() || isWaiting) return;
|
if (!question.trim() || isWaiting) return;
|
||||||
|
|
||||||
const userMessage = { sender: "user", text: question };
|
const userMessage = { sender: "user", text: question, id: newChatMessageId() };
|
||||||
setMessages((prev) => [...prev, userMessage]);
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
setQuestion("");
|
setQuestion("");
|
||||||
setIsWaiting(true);
|
setIsWaiting(true);
|
||||||
@ -66,6 +95,7 @@ export default function ChatBot({ onClose }) {
|
|||||||
sources: payload.sources || [],
|
sources: payload.sources || [],
|
||||||
grounded: Boolean(payload.grounded),
|
grounded: Boolean(payload.grounded),
|
||||||
timeout: Boolean(payload._timeout),
|
timeout: Boolean(payload._timeout),
|
||||||
|
id: newChatMessageId(),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@ -77,6 +107,7 @@ export default function ChatBot({ onClose }) {
|
|||||||
sources: [],
|
sources: [],
|
||||||
grounded: false,
|
grounded: false,
|
||||||
error: true,
|
error: true,
|
||||||
|
id: newChatMessageId(),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
@ -109,6 +140,19 @@ export default function ChatBot({ onClose }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -124,6 +168,7 @@ export default function ChatBot({ onClose }) {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
@ -136,10 +181,11 @@ export default function ChatBot({ onClose }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{messages.map((msg, index) => {
|
{messages.map((msg, index) => {
|
||||||
|
const msgKey = msg.id ?? `legacy-${index}`;
|
||||||
if (msg.sender === "user") {
|
if (msg.sender === "user") {
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
className="ml-auto max-w-[80%] rounded-sheet bg-primary px-3 py-2 font-headline text-xs leading-relaxed text-white"
|
||||||
>
|
>
|
||||||
{msg.text}
|
{msg.text}
|
||||||
@ -147,7 +193,7 @@ export default function ChatBot({ onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
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
|
<div
|
||||||
className="rounded-sheet bg-surface-container px-3 py-2 text-on-surface [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
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