| import React, { useState, useRef, useEffect } from 'react'; |
| import ReactMarkdown from 'react-markdown'; |
| import Avatar from './Avatar.jsx'; |
| import '../App.css'; |
|
|
| const ChatInterface = ({ messages = [], setMessages = () => {}, onMessageSent = () => {}, activeConversationId, |
| saveBotResponse, toLogin, onCreateNewConversation = () => {},onNewChat = () => {},refreshConversationList = () => {} }) => { |
| const [inputMessage, setInputMessage] = useState(''); |
| const [isLoading, setIsLoading] = useState(false); |
| const messagesEndRef = useRef(null); |
| const textareaRef = useRef(null); |
| const [streamingText, setStreamingText] = useState(''); |
| const [isStreaming, setIsStreaming] = useState(false); |
| const [fullResponse, setFullResponse] = useState(''); |
| const [tokenLimitReached, setTokenLimitReached] = useState(false); |
| const [hasInteractionStarted, setHasInteractionStarted] = useState(false); |
|
|
| const scrollToBottom = () => { |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| }; |
| useEffect(scrollToBottom, [messages]); |
|
|
|
|
|
|
| const streamResponse = (response) => { |
| setIsStreaming(true); |
| setFullResponse(response); |
| setStreamingText(''); |
| |
| |
| let streamMessageId = Date.now().toString(); |
| |
| setMessages(prev => [...prev, { |
| sender: 'bot-streaming', |
| text: '', |
| id: streamMessageId |
| }]); |
| |
| const totalCharacters = response.length; |
| let charCount = 0; |
| |
| const streamInterval = setInterval(() => { |
| if (charCount < totalCharacters) { |
| charCount += 5; |
| const fragment = response.substring(0, charCount); |
| |
| setMessages(prev => |
| prev.map(msg => |
| msg.id === streamMessageId ? { ...msg, text: fragment } : msg |
| ) |
| ); |
| |
| setStreamingText(fragment); |
| } else { |
| clearInterval(streamInterval); |
| setIsStreaming(false); |
| |
| setMessages(prev => |
| prev.map(msg => |
| msg.id === streamMessageId |
| ? { sender: 'bot', text: response, id: streamMessageId } |
| : msg |
| ) |
| ); |
| } |
| }, 30); |
| |
| return () => clearInterval(streamInterval); |
| }; |
|
|
| |
| const sendMessage = async (message) => { |
| try { |
| setHasInteractionStarted(true); |
| setIsLoading(true); |
| |
| setMessages(prev => [...prev, { sender: 'user', text: message }]); |
| |
| const updatedConversationId = await onMessageSent(message); |
| |
| const chatRes = await fetch('/api/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| credentials: 'include', |
| body: JSON.stringify({ |
| message, |
| conversation_id: activeConversationId, |
| skip_save: true |
| }), |
| }); |
| |
| const responseData = await chatRes.json(); |
| |
| if (responseData.error === 'token_limit_exceeded') { |
| setIsLoading(false); |
| setTokenLimitReached(true); |
| |
| setMessages(prev => [...prev, { |
| sender: 'bot', |
| text: "⚠️ **Limite de taille de conversation atteinte**\n\nCette conversation est devenue trop longue. Pour continuer à discuter, veuillez créer une nouvelle conversation." |
| }]); |
| |
| return; |
| } |
| |
| if (!chatRes.ok) throw new Error(`Chat API error ${chatRes.status}`); |
| |
| const { response: botResponse } = responseData; |
| |
| setIsLoading(false); |
| |
| streamResponse(botResponse); |
| |
| |
| |
| if (activeConversationId && typeof refreshConversationList === 'function') { |
| refreshConversationList(); |
| } |
| |
| if (updatedConversationId) { |
| saveBotResponse(updatedConversationId, botResponse, true); |
| } else if (activeConversationId) { |
| saveBotResponse(activeConversationId, botResponse, true); |
| } |
| |
| } catch (error) { |
| console.error('Erreur:', error); |
| setIsLoading(false); |
| setMessages(prev => [...prev, |
| { sender: 'bot', text: "Désolé, une erreur s'est produite. Veuillez réessayer." } |
| ]); |
| } |
| }; |
|
|
| const handleCreateNewConversation = () => { |
| onNewChat(); |
| setTokenLimitReached(false); |
| setHasInteractionStarted(false); |
|
|
| }; |
| useEffect(() => { |
| if (activeConversationId === null && messages.length === 0) { |
| setHasInteractionStarted(false); |
| } |
| }, [activeConversationId, messages]); |
|
|
|
|
|
|
| const handleSubmit = (e) => { |
| e.preventDefault(); |
| const txt = inputMessage.trim(); |
| if (!txt) return; |
| sendMessage(txt); |
| setInputMessage(''); |
| if (textareaRef.current) textareaRef.current.style.height = 'auto'; |
| }; |
|
|
|
|
| const isMarkdown = (text) => { |
| return /(?:\*\*|__|##|\*|_|`|>|\d+\.\s|\-\s|\[.*\]\(.*\))/.test(text); |
| }; |
| return ( |
| <div className="chat-container"> |
| {messages.length === 0 && !hasInteractionStarted ? ( |
| <> |
| <div className="chat-header"> |
| <h2 className="chat-title">Medic.ial</h2> |
| <Avatar onClick={toLogin} /> |
| </div> |
| <div className="no-messages-view"> |
| <div className="welcome-content"> |
| <div className="welcome-message"> |
| <p>Bonjour ! Comment puis-je vous aider aujourd'hui ? 🧑⚕️</p> |
| </div> |
| <div className="input-container centered"> |
| <form onSubmit={handleSubmit} className="input-form"> |
| <textarea |
| value={inputMessage} |
| onChange={(e) => setInputMessage(e.target.value)} |
| placeholder="Posez une question..." |
| disabled={isLoading} |
| rows="1" |
| ref={textareaRef} |
| className="input-textarea" |
| onKeyDown={(e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| handleSubmit(e); |
| } |
| }} |
| onInput={(e) => { |
| e.target.style.height = 'auto'; |
| e.target.style.height = `${e.target.scrollHeight}px`; |
| }} |
| /> |
| <button type="submit" disabled={isLoading || !inputMessage.trim()}> |
| <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"> |
| <path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/> |
| </svg> |
| </button> |
| </form> |
| </div> |
| </div> |
| </div> |
| </> |
| ) : ( |
| <> |
| <div className="chat-header"> |
| <Avatar onClick={toLogin} /> |
| <h2 className="chat-title">Medic.ial</h2> |
| </div> |
| <div className="messages-container"> |
| {messages.map((msg, index) => { |
| if (msg.sender === 'bot-streaming') return null; |
| |
| return ( |
| <div key={index} className={`message ${msg.sender}`}> |
| <div className="message-content"> |
| {isMarkdown(msg.text) ? <ReactMarkdown>{msg.text}</ReactMarkdown> : msg.text} |
| </div> |
| </div> |
| ); |
| })} |
| {tokenLimitReached && ( |
| <div className="token-limit-warning"> |
| <button |
| className="new-conversation-button" |
| onClick={handleCreateNewConversation} |
| > |
| Démarrer une nouvelle conversation |
| </button> |
| </div> |
| )} |
| {isStreaming && ( |
| <div className="message bot"> |
| <div className="message-content streaming-message"> |
| {isMarkdown(streamingText) ? <ReactMarkdown>{streamingText}</ReactMarkdown> : streamingText} |
| </div> |
| </div> |
| )} |
| |
| {isLoading && ( |
| <div className="message bot"> |
| <div className="message-content loading"> |
| <span>.</span><span>.</span><span>.</span> |
| </div> |
| </div> |
| )} |
| <div ref={messagesEndRef} /> |
| </div> |
| <div className="input-container"> |
| <form onSubmit={handleSubmit} className="input-form"> |
| <textarea |
| value={inputMessage} |
| onChange={(e) => setInputMessage(e.target.value)} |
| placeholder={tokenLimitReached ? "Créez une nouvelle conversation pour continuer..." : "Tapez votre message ici..."} |
| disabled={isLoading || tokenLimitReached} |
| rows="1" |
| ref={textareaRef} |
| className="input-textarea" |
| onKeyDown={(e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| handleSubmit(e); |
| } |
| }} |
| onInput={(e) => { |
| e.target.style.height = 'auto'; |
| e.target.style.height = `${e.target.scrollHeight}px`; |
| }} |
| /> |
| <button type="submit" disabled={isLoading || !inputMessage.trim() || tokenLimitReached}> |
| <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"> |
| <path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/> |
| </svg> |
| </button> |
| </form> |
| <figcaption className="disclaimer-text"> |
| Medic.ial est sujet à faire des erreurs. Vérifiez les informations fournies. |
| </figcaption> |
| </div> |
| </> |
| )} |
| </div> |
| ); |
| }; |
|
|
| export default ChatInterface; |