AxL95 commited on
Commit
fd2e6bc
·
verified ·
1 Parent(s): 085905a

Update frontend/src/components/ChatInterface.jsx

Browse files
frontend/src/components/ChatInterface.jsx CHANGED
@@ -3,275 +3,132 @@ import ReactMarkdown from 'react-markdown';
3
  import Avatar from './Avatar.jsx';
4
  import '../App.css';
5
 
6
- const ChatInterface = ({
7
- messages = [],
8
- setMessages = () => {},
9
- onMessageSent = () => {},
10
- activeConversationId,
11
- saveBotResponse,
12
- toLogin,
13
- onNewChat = () => {},
14
- refreshConversationList = () => {}
15
- }) => {
16
  const [inputMessage, setInputMessage] = useState('');
17
  const [isLoading, setIsLoading] = useState(false);
18
  const messagesEndRef = useRef(null);
19
  const textareaRef = useRef(null);
 
20
  const [isStreaming, setIsStreaming] = useState(false);
 
21
  const [tokenLimitReached, setTokenLimitReached] = useState(false);
22
  const [hasInteractionStarted, setHasInteractionStarted] = useState(false);
23
- const [currentStreamId, setCurrentStreamId] = useState(null);
24
-
25
- // Pour optimiser les performances du streaming
26
- const accumulatedText = useRef('');
27
- const updateThreshold = 1;
28
- const updateIntervalRef = useRef(null);
29
 
30
  const scrollToBottom = () => {
31
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
32
  };
33
-
34
- useEffect(() => {
35
- scrollToBottom();
 
 
 
 
 
36
 
37
- // Nettoyer l'intervalle précédent si existant
38
- if (updateIntervalRef.current) {
39
- clearInterval(updateIntervalRef.current);
40
- updateIntervalRef.current = null;
41
- }
42
 
43
- // Créer un nouvel intervalle si en streaming
44
- if (isStreaming && currentStreamId) {
45
- updateIntervalRef.current = setInterval(() => {
46
- if (accumulatedText.current.length > 0) {
47
- setMessages(prev => {
48
- return prev.map(msg =>
49
- msg.id === currentStreamId
50
- ? { ...msg, text: msg.text + accumulatedText.current }
51
- : msg
52
- );
53
- });
54
- accumulatedText.current = '';
55
- }
56
- }, 100); // Mise à jour toutes les 100ms pour plus de fluidité
57
- }
58
 
59
- // Nettoyage lors du démontage
60
- return () => {
61
- if (updateIntervalRef.current) {
62
- clearInterval(updateIntervalRef.current);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
- };
65
- }, [isStreaming, currentStreamId, messages]);
66
- const sendMessage = async (message) => {
 
 
 
 
67
  try {
68
  setHasInteractionStarted(true);
69
  setIsLoading(true);
70
- const userMessageId = `user-${Date.now()}`;
71
-
72
- // Ajouter le message de l'utilisateur
73
- setMessages(prev => [...prev, {
74
- sender: 'user',
75
- text: message,
76
- id: userMessageId
77
- }]);
78
- // Envoyer le message au backend
79
- const updatedConversationId = await onMessageSent(message);
80
 
81
- // Créer un ID unique pour le message en streaming
82
- const streamMessageId = `bot-${Date.now()}`;
83
- setCurrentStreamId(streamMessageId);
84
-
85
- // Ajouter un message bot vide pour le streaming
86
- setMessages(prev => {
87
- // Vérifier si le message utilisateur existe déjà
88
- const userMessageExists = prev.some(m => m.id === userMessageId);
89
-
90
- // Si pour une raison quelconque le message utilisateur a disparu, le rajouter
91
- const updatedMessages = userMessageExists ? prev : [
92
- ...prev,
93
- { sender: 'user', text: message, id: userMessageId }
94
- ];
95
-
96
- // Ajouter le message bot de streaming
97
- return [...updatedMessages, {
98
- sender: 'bot',
99
- text: '',
100
- id: streamMessageId
101
- }];
102
- });
103
 
104
- setIsLoading(false); // Plus de chargement mais streaming
105
- setIsStreaming(true); // Commencer le streaming
106
 
107
- // Faire la requête au backend
108
- const response = await fetch('/api/chat', {
109
  method: 'POST',
110
  headers: { 'Content-Type': 'application/json' },
111
  credentials: 'include',
112
  body: JSON.stringify({
113
  message,
114
- conversation_id: activeConversationId || updatedConversationId,
115
- skip_save: false // Le backend gère la sauvegarde
116
  }),
117
  });
118
 
119
- // Gestion des erreurs HTTP
120
- if (!response.ok) {
121
- const errorData = await response.json();
122
-
123
- if (errorData.error === 'token_limit_exceeded') {
124
- setIsStreaming(false);
125
- setCurrentStreamId(null);
126
- setTokenLimitReached(true);
127
-
128
- setMessages(prev => [...prev.filter(m => m.id !== streamMessageId), {
129
- sender: 'bot',
130
- text: "⚠️ **Limite de taille de conversation atteinte**\n\nCette conversation est devenue trop longue. Pour continuer à discuter, veuillez créer une nouvelle conversation."
131
- }]);
132
-
133
- return;
134
- }
135
-
136
- throw new Error(`Chat API error ${response.status}`);
137
- }
138
 
139
- // Traiter la réponse en streaming si disponible
140
- if (response.headers.get('content-type')?.includes('text/event-stream') && response.body) {
141
- const reader = response.body.getReader();
142
- const decoder = new TextDecoder();
143
- let fullText = '';
144
-
145
- while (true) {
146
- const { done, value } = await reader.read();
147
- if (done) break;
148
-
149
- // Décoder et traiter les données
150
- const chunk = decoder.decode(value);
151
- const lines = chunk.split('\n\n');
152
-
153
- for (const line of lines) {
154
- if (line.startsWith('data: ')) {
155
- try {
156
- const data = JSON.parse(line.slice(5));
157
-
158
- if (data.type === 'start') {
159
- console.log("Début du streaming");
160
- fullText = '';
161
- accumulatedText.current = '';
162
- }
163
- else if (data.type === 'end') {
164
- console.log("SSE End received");
165
- setIsStreaming(false);
166
- setCurrentStreamId(null);
167
- setIsLoading(false);
168
-
169
- // Mise à jour finale
170
- setMessages(prev =>
171
- prev.map(msg =>
172
- msg.id === streamMessageId
173
- ? { ...msg, sender: 'bot', text: fullText }
174
- : msg
175
- )
176
- );
177
-
178
- // Rafraîchir la liste des conversations
179
- if (typeof refreshConversationList === 'function') {
180
- setTimeout(refreshConversationList, 100);
181
- }
182
-
183
- return; // Sortir de la boucle une fois terminé
184
- }
185
- else if (data.content) {
186
- // Ajouter le contenu du chunk
187
- fullText += data.content;
188
- accumulatedText.current += data.content;
189
-
190
- // Mise à jour moins fréquente de l'interface
191
- if (accumulatedText.current.length >= updateThreshold || data.content.includes('\n')) {
192
- setMessages(prev => {
193
- const userMsg = prev.find(m => m.sender === 'user' && m.text === message);
194
- const botMsg = prev.find(m => m.id === streamMessageId);
195
-
196
- const updatedMessages = userMsg ? prev : [
197
- ...prev,
198
- { sender: 'user', text: message, id: `user-${Date.now()}` }
199
- ];
200
-
201
- if (botMsg) {
202
- return updatedMessages.map(msg =>
203
- msg.id === streamMessageId ? { ...msg, text: fullText } : msg
204
- );
205
- } else {
206
- return [...updatedMessages, { sender: 'bot', text: fullText, id: streamMessageId }];
207
- }
208
- });
209
-
210
- accumulatedText.current = ''; // Réinitialiser l'accumulateur
211
-
212
- // Faire défiler vers le bas
213
- requestAnimationFrame(() => {
214
- scrollToBottom();
215
- });
216
- }
217
- }
218
- else if (data.type === 'error') {
219
- console.error("SSE Error received:", data.error);
220
- setIsStreaming(false);
221
- setCurrentStreamId(null);
222
- setIsLoading(false);
223
-
224
- // Remplacer par un message d'erreur
225
- setMessages(prev =>
226
- prev.map(msg =>
227
- msg.id === streamMessageId
228
- ? { sender: 'bot', text: "Désolé, une erreur s'est produite." }
229
- : msg
230
- )
231
- );
232
- }
233
- } catch (e) {
234
- console.error('Error parsing SSE data:', e, line);
235
- }
236
- }
237
- }
238
- }
239
- }
240
- else {
241
- // Fallback pour les réponses non-streaming
242
- console.log("Received non-streaming response.");
243
- const responseData = await response.json();
244
- setIsStreaming(false);
245
- setCurrentStreamId(null);
246
  setIsLoading(false);
 
247
 
248
- setMessages(prev =>
249
- prev.map(msg =>
250
- msg.id === streamMessageId
251
- ? { sender: 'bot', text: responseData.response, id: streamMessageId }
252
- : msg
253
- )
254
- );
255
 
256
- // Rafraîchir la liste des conversations
257
- if (typeof refreshConversationList === 'function') {
258
- setTimeout(refreshConversationList, 100);
259
- }
260
  }
261
 
262
- } catch (error) {
263
- console.error('Erreur lors de l\'envoi/réception du message:', error);
264
- setIsStreaming(false);
265
- setCurrentStreamId(null);
266
  setIsLoading(false);
267
 
268
- // Afficher l'erreur
269
- setMessages(prev => {
270
- const filteredMessages = prev.filter(m => m.id !== currentStreamId);
271
- return [...filteredMessages,
272
- { sender: 'bot', text: "Désolé, une erreur s'est produite. Veuillez réessayer." }
273
- ];
274
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  }
276
  };
277
 
@@ -279,14 +136,16 @@ const ChatInterface = ({
279
  onNewChat();
280
  setTokenLimitReached(false);
281
  setHasInteractionStarted(false);
 
282
  };
283
-
284
  useEffect(() => {
285
  if (activeConversationId === null && messages.length === 0) {
286
  setHasInteractionStarted(false);
287
  }
288
  }, [activeConversationId, messages]);
289
 
 
 
290
  const handleSubmit = (e) => {
291
  e.preventDefault();
292
  const txt = inputMessage.trim();
@@ -296,16 +155,10 @@ const ChatInterface = ({
296
  if (textareaRef.current) textareaRef.current.style.height = 'auto';
297
  };
298
 
299
- const isMarkdown = (text, sender) => {
300
- // Forcer le rendu Markdown pour TOUS les messages du bot
301
- if (sender === 'bot') {
302
- return true;
303
- }
304
- // Pour les messages utilisateur, vérifier la présence de syntaxe Markdown
305
- return /(?:\*\*|__|##|\*|_|`|>|\d+\.\s|\-\s|\[.*\]\(.*\))/.test(text);
306
- };
307
-
308
-
309
  return (
310
  <div className="chat-container">
311
  {messages.length === 0 && !hasInteractionStarted ? (
@@ -358,24 +211,17 @@ const ChatInterface = ({
358
  </div>
359
  <div className="messages-container">
360
  {messages.map((msg, index) => {
361
- const isActiveStreaming = isStreaming && msg.id === currentStreamId;
362
-
363
- return (
364
- <div key={msg.id || index} className={`message ${msg.sender}`}>
365
- <div className={`message-content ${isActiveStreaming ? 'streaming-message' : ''}`}>
366
- {isMarkdown(msg.text, msg.sender) ?
367
- <ReactMarkdown>{msg.text}</ReactMarkdown> :
368
- <span>{msg.text}</span>
369
- }
370
- {isActiveStreaming && (
371
- <span className="typing-indicator">▌</span>
372
- )}
373
- </div>
374
- </div>
375
- );
376
- })}
377
-
378
- {tokenLimitReached && (
379
  <div className="token-limit-warning">
380
  <button
381
  className="new-conversation-button"
@@ -385,6 +231,13 @@ const ChatInterface = ({
385
  </button>
386
  </div>
387
  )}
 
 
 
 
 
 
 
388
 
389
  {isLoading && (
390
  <div className="message bot">
@@ -417,7 +270,7 @@ const ChatInterface = ({
417
  }}
418
  />
419
  <button type="submit" disabled={isLoading || !inputMessage.trim() || tokenLimitReached}>
420
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3">
421
  <path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/>
422
  </svg>
423
  </button>
 
3
  import Avatar from './Avatar.jsx';
4
  import '../App.css';
5
 
6
+ const ChatInterface = ({ messages = [], setMessages = () => {}, onMessageSent = () => {}, activeConversationId,
7
+ saveBotResponse, toLogin, onCreateNewConversation = () => {},onNewChat = () => {},refreshConversationList = () => {} }) => {
 
 
 
 
 
 
 
 
8
  const [inputMessage, setInputMessage] = useState('');
9
  const [isLoading, setIsLoading] = useState(false);
10
  const messagesEndRef = useRef(null);
11
  const textareaRef = useRef(null);
12
+ const [streamingText, setStreamingText] = useState('');
13
  const [isStreaming, setIsStreaming] = useState(false);
14
+ const [fullResponse, setFullResponse] = useState('');
15
  const [tokenLimitReached, setTokenLimitReached] = useState(false);
16
  const [hasInteractionStarted, setHasInteractionStarted] = useState(false);
 
 
 
 
 
 
17
 
18
  const scrollToBottom = () => {
19
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
20
  };
21
+ useEffect(scrollToBottom, [messages]);
22
+
23
+
24
+
25
+ const streamResponse = (response) => {
26
+ setIsStreaming(true);
27
+ setFullResponse(response);
28
+ setStreamingText('');
29
 
30
+ // Garder une référence au message de streaming
31
+ let streamMessageId = Date.now().toString();
 
 
 
32
 
33
+ setMessages(prev => [...prev, {
34
+ sender: 'bot-streaming',
35
+ text: '',
36
+ id: streamMessageId
37
+ }]);
38
+
39
+ const totalCharacters = response.length;
40
+ let charCount = 0;
 
 
 
 
 
 
 
41
 
42
+ const streamInterval = setInterval(() => {
43
+ if (charCount < totalCharacters) {
44
+ charCount += 5;
45
+ const fragment = response.substring(0, charCount);
46
+
47
+ setMessages(prev =>
48
+ prev.map(msg =>
49
+ msg.id === streamMessageId ? { ...msg, text: fragment } : msg
50
+ )
51
+ );
52
+
53
+ setStreamingText(fragment);
54
+ } else {
55
+ clearInterval(streamInterval);
56
+ setIsStreaming(false);
57
+
58
+ setMessages(prev =>
59
+ prev.map(msg =>
60
+ msg.id === streamMessageId
61
+ ? { sender: 'bot', text: response, id: streamMessageId }
62
+ : msg
63
+ )
64
+ );
65
  }
66
+ }, 30);
67
+
68
+ return () => clearInterval(streamInterval);
69
+ };
70
+
71
+
72
+ const sendMessage = async (message) => {
73
  try {
74
  setHasInteractionStarted(true);
75
  setIsLoading(true);
 
 
 
 
 
 
 
 
 
 
76
 
77
+ setMessages(prev => [...prev, { sender: 'user', text: message }]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ const updatedConversationId = await onMessageSent(message);
 
80
 
81
+ const chatRes = await fetch('/api/chat', {
 
82
  method: 'POST',
83
  headers: { 'Content-Type': 'application/json' },
84
  credentials: 'include',
85
  body: JSON.stringify({
86
  message,
87
+ conversation_id: activeConversationId,
88
+ skip_save: true
89
  }),
90
  });
91
 
92
+ const responseData = await chatRes.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ if (responseData.error === 'token_limit_exceeded') {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  setIsLoading(false);
96
+ setTokenLimitReached(true);
97
 
98
+ setMessages(prev => [...prev, {
99
+ sender: 'bot',
100
+ text: "⚠️ **Limite de taille de conversation atteinte**\n\nCette conversation est devenue trop longue. Pour continuer à discuter, veuillez créer une nouvelle conversation."
101
+ }]);
 
 
 
102
 
103
+ return;
 
 
 
104
  }
105
 
106
+ if (!chatRes.ok) throw new Error(`Chat API error ${chatRes.status}`);
107
+
108
+ const { response: botResponse } = responseData;
109
+
110
  setIsLoading(false);
111
 
112
+ streamResponse(botResponse);
113
+
114
+
115
+
116
+ if (activeConversationId && typeof refreshConversationList === 'function') {
117
+ refreshConversationList();
118
+ }
119
+
120
+ if (updatedConversationId) {
121
+ saveBotResponse(updatedConversationId, botResponse, true);
122
+ } else if (activeConversationId) {
123
+ saveBotResponse(activeConversationId, botResponse, true);
124
+ }
125
+
126
+ } catch (error) {
127
+ console.error('Erreur:', error);
128
+ setIsLoading(false);
129
+ setMessages(prev => [...prev,
130
+ { sender: 'bot', text: "Désolé, une erreur s'est produite. Veuillez réessayer." }
131
+ ]);
132
  }
133
  };
134
 
 
136
  onNewChat();
137
  setTokenLimitReached(false);
138
  setHasInteractionStarted(false);
139
+
140
  };
 
141
  useEffect(() => {
142
  if (activeConversationId === null && messages.length === 0) {
143
  setHasInteractionStarted(false);
144
  }
145
  }, [activeConversationId, messages]);
146
 
147
+
148
+
149
  const handleSubmit = (e) => {
150
  e.preventDefault();
151
  const txt = inputMessage.trim();
 
155
  if (textareaRef.current) textareaRef.current.style.height = 'auto';
156
  };
157
 
158
+
159
+ const isMarkdown = (text) => {
160
+ return /(?:\*\*|__|##|\*|_|`|>|\d+\.\s|\-\s|\[.*\]\(.*\))/.test(text);
161
+ };
 
 
 
 
 
 
162
  return (
163
  <div className="chat-container">
164
  {messages.length === 0 && !hasInteractionStarted ? (
 
211
  </div>
212
  <div className="messages-container">
213
  {messages.map((msg, index) => {
214
+ if (msg.sender === 'bot-streaming') return null;
215
+
216
+ return (
217
+ <div key={index} className={`message ${msg.sender}`}>
218
+ <div className="message-content">
219
+ {isMarkdown(msg.text) ? <ReactMarkdown>{msg.text}</ReactMarkdown> : msg.text}
220
+ </div>
221
+ </div>
222
+ );
223
+ })}
224
+ {tokenLimitReached && (
 
 
 
 
 
 
 
225
  <div className="token-limit-warning">
226
  <button
227
  className="new-conversation-button"
 
231
  </button>
232
  </div>
233
  )}
234
+ {isStreaming && (
235
+ <div className="message bot">
236
+ <div className="message-content streaming-message">
237
+ {isMarkdown(streamingText) ? <ReactMarkdown>{streamingText}</ReactMarkdown> : streamingText}
238
+ </div>
239
+ </div>
240
+ )}
241
 
242
  {isLoading && (
243
  <div className="message bot">
 
270
  }}
271
  />
272
  <button type="submit" disabled={isLoading || !inputMessage.trim() || tokenLimitReached}>
273
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3">
274
  <path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/>
275
  </svg>
276
  </button>