diff --git a/app/api/proxy/route.js b/app/api/proxy/route.js new file mode 100644 index 0000000..fc0be07 --- /dev/null +++ b/app/api/proxy/route.js @@ -0,0 +1,31 @@ +export async function GET(req) { + const { searchParams } = new URL(req.url); + const question = searchParams.get("q"); + + if (!question) { + return new Response(JSON.stringify({ error: "Question manquante" }), { status: 400 }); + } + + const apiUrl = `https://llmapi.fernandgrascalvet.com/ask?q=${encodeURIComponent(question)}`; + + try { + const response = await fetch(apiUrl, { + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + return new Response(JSON.stringify(data), { + status: response.status, + headers: { + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", + }, + }); + } catch (error) { + return new Response(JSON.stringify({ error: "Erreur de communication avec l'API" }), { + status: 500, + }); + } +} diff --git a/app/components/ChatBot.js b/app/components/ChatBot.js index 34acfea..1525d04 100644 --- a/app/components/ChatBot.js +++ b/app/components/ChatBot.js @@ -1,27 +1,76 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { askAI } from "../utils/askAI"; export default function ChatBot() { const [question, setQuestion] = useState(""); - const [response, setResponse] = useState(""); + const [messages, setMessages] = useState([]); + + // Afficher le message d'introduction une seule fois au chargement du chatbot + useEffect(() => { + const introMessage = { + sender: "bot", + text: "Bonjour ! Je suis GrasBot, une intelligence artificielle locale basée sur Mistral 7B et hébergée sur un serveur Windows. Je suis là pour répondre à vos questions. Posez-moi votre question ! 😊" + }; + setMessages([introMessage]); + }, []); const handleAsk = async () => { - if (!question) return; - const aiResponse = await askAI(question); - setResponse(aiResponse); + if (!question.trim()) return; // Évite d'envoyer un message vide + + const userMessage = { sender: "user", text: question }; + setMessages((prevMessages) => [...prevMessages, userMessage]); // Ajoute le message utilisateur + + setQuestion(""); // Réinitialise le champ après l'envoi + + try { + const botResponse = await askAI(question); + const botMessage = { sender: "bot", text: botResponse }; + setMessages((prevMessages) => [...prevMessages, botMessage]); // Ajoute la réponse du bot + } catch (error) { + setMessages((prevMessages) => [ + ...prevMessages, + { sender: "bot", text: "❌ Erreur de réponse. Réessayez plus tard." } + ]); + } }; return ( -
-

Chat avec l'IA

- setQuestion(e.target.value)} - /> - - {response &&

Réponse : {response}

} +
+ {/* En-tête du chatbot */} +
+ 💬 GrasBot + +
+ + {/* Zone d'affichage des messages */} +
+ {messages.map((msg, index) => ( +
+ {msg.text} +
+ ))} +
+ + {/* Zone d'entrée utilisateur */} +
+ setQuestion(e.target.value)} + /> + +
); } diff --git a/app/components/ContentSectionCompetences.tsx b/app/components/ContentSectionCompetences.tsx index 19e62a2..a5106e4 100644 --- a/app/components/ContentSectionCompetences.tsx +++ b/app/components/ContentSectionCompetences.tsx @@ -6,6 +6,7 @@ import CarouselCompetences from "./CarouselCompetences"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import ModalGlossaire from "./ModalGlossaire"; +import ChatBot from "./ChatBot"; // ✅ Import du ChatBot // ✅ Définition des types pour TypeScript interface ImageData { @@ -46,16 +47,16 @@ export default function ContentSectionCompetences({ console.log("🔍 [ContentSectionCompetences] Chargement du composant..."); const [selectedMot, setSelectedMot] = useState(null); - const [loading, setLoading] = useState(competenceData === null); // ✅ Initialiser correctement loading + const [isChatbotOpen, setIsChatbotOpen] = useState(false); // ✅ État pour afficher/masquer le chatbot + const [loading, setLoading] = useState(competenceData === null); const apiUrl = getApiUrl(); useEffect(() => { if (competenceData) { - setLoading(false); // ✅ Mise à jour de loading une seule fois après chargement + setLoading(false); } }, [competenceData]); - // ✅ Affichage d'un message de chargement if (loading) { return
⏳ Chargement des détails de la compétence...
; } @@ -67,7 +68,6 @@ export default function ContentSectionCompetences({ const { name, content, picture } = competenceData; - // ✅ Transformation des images de Strapi en format attendu par le carrousel const images = picture?.map((img) => ({ url: `${apiUrl}${img.formats?.large?.url || img.url}`, @@ -76,11 +76,17 @@ export default function ContentSectionCompetences({ console.log("✅ [ContentSectionCompetences] Images préparées :", images); - // ✅ Transformation des mots-clés du glossaire function transformMarkdownWithKeywords(text: string) { if (!glossaireData.length) return text; let modifiedText = text; + + // ✅ Ajout de la mise en surbrillance pour "IA locale" + modifiedText = modifiedText.replace( + /\bIA locale\b/g, + `IA locale` + ); + glossaireData.forEach(({ mot_clef, variantes }) => { const regexVariants = variantes .map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) @@ -98,7 +104,7 @@ export default function ContentSectionCompetences({ const contentWithLinks = transformMarkdownWithKeywords(content); - // ✅ Gestion des clics sur les mots-clés + // ✅ Gestion des clics sur les mots-clés pour le glossaire useEffect(() => { function handleKeywordClick(event: MouseEvent) { const target = event.target as HTMLElement; @@ -115,14 +121,36 @@ export default function ContentSectionCompetences({ return () => document.body.removeEventListener("click", handleKeywordClick); }, [glossaireData]); + // ✅ Gestion du clic sur "IA locale" pour ouvrir le chatbot + useEffect(() => { + function handleChatbotClick(event: MouseEvent) { + const target = event.target as HTMLElement; + if (target.dataset.chatbot === "true") { + setIsChatbotOpen(true); + } + } + + document.body.addEventListener("click", handleChatbotClick); + return () => document.body.removeEventListener("click", handleChatbotClick); + }, []); + return (
-

{name}

+

+ {name} +

{contentWithLinks}
{selectedMot && setSelectedMot(null)} />} + + {/* 🔥 Chatbot affiché uniquement si isChatbotOpen est vrai */} + {isChatbotOpen && ( +
+ +
+ )}
); } diff --git a/app/utils/askAI.js b/app/utils/askAI.js index e69de29..7214a06 100644 --- a/app/utils/askAI.js +++ b/app/utils/askAI.js @@ -0,0 +1,5 @@ +export async function askAI(question) { + const response = await fetch(`/api/proxy?q=${encodeURIComponent(question)}`); + const data = await response.json(); + return data.response; +} diff --git a/llm-api/__pycache__/api.cpython-313.pyc b/llm-api/__pycache__/api.cpython-313.pyc index 45388e9..ffddb41 100644 Binary files a/llm-api/__pycache__/api.cpython-313.pyc and b/llm-api/__pycache__/api.cpython-313.pyc differ