AxL95 commited on
Commit
03c52a3
·
verified ·
1 Parent(s): 0a3b5e1

Update frontend/src/components/ChatInterface.jsx

Browse files
frontend/src/components/ChatInterface.jsx CHANGED
@@ -3,132 +3,252 @@ import ReactMarkdown from 'react-markdown';
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,16 +256,14 @@ saveBotResponse, toLogin, onCreateNewConversation = () => {},onNewChat = () => {
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,10 +273,14 @@ saveBotResponse, toLogin, onCreateNewConversation = () => {},onNewChat = () => {
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,17 +333,24 @@ const isMarkdown = (text) => {
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,13 +360,6 @@ const isMarkdown = (text) => {
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,7 +392,7 @@ const isMarkdown = (text) => {
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>
@@ -285,4 +407,4 @@ const isMarkdown = (text) => {
285
  );
286
  };
287
 
288
- export default ChatInterface;
 
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
+ if (updateIntervalRef.current) {
38
+ clearInterval(updateIntervalRef.current);
39
+ updateIntervalRef.current = null;
40
+ }
 
41
 
42
+ if (isStreaming && currentStreamId) {
43
+ updateIntervalRef.current = setInterval(() => {
44
+ if (accumulatedText.current.length > 0) {
45
+ setMessages(prev => {
46
+ return prev.map(msg =>
47
+ msg.id === currentStreamId
48
+ ? { ...msg, text: msg.text + accumulatedText.current }
49
+ : msg
50
+ );
51
+ });
52
+ accumulatedText.current = '';
53
+ }
54
+ }, 100);
55
+ }
56
 
57
+ return () => {
58
+ if (updateIntervalRef.current) {
59
+ clearInterval(updateIntervalRef.current);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
+ };
62
+ }, [isStreaming, currentStreamId, messages]);
63
+ const sendMessage = async (message) => {
 
 
 
 
64
  try {
65
  setHasInteractionStarted(true);
66
  setIsLoading(true);
67
+ const userMessageId = `user-${Date.now()}`;
68
+
69
+ setMessages(prev => [...prev, {
70
+ sender: 'user',
71
+ text: message,
72
+ id: userMessageId
73
+ }]);
74
+ const updatedConversationId = await onMessageSent(message);
75
 
76
+ const streamMessageId = `bot-${Date.now()}`;
77
+ setCurrentStreamId(streamMessageId);
78
 
79
+ setMessages(prev => {
80
+ const userMessageExists = prev.some(m => m.id === userMessageId);
81
+
82
+ const updatedMessages = userMessageExists ? prev : [
83
+ ...prev,
84
+ { sender: 'user', text: message, id: userMessageId }
85
+ ];
86
+
87
+ return [...updatedMessages, {
88
+ sender: 'bot',
89
+ text: '',
90
+ id: streamMessageId
91
+ }];
92
+ });
93
 
94
+ setIsLoading(false);
95
+ setIsStreaming(true);
96
+
97
+ const response = await fetch('http://localhost:8000/api/chat', {
98
  method: 'POST',
99
  headers: { 'Content-Type': 'application/json' },
100
  credentials: 'include',
101
  body: JSON.stringify({
102
  message,
103
+ conversation_id: activeConversationId || updatedConversationId,
104
+ skip_save: false // Le backend gère la sauvegarde
105
  }),
106
  });
107
 
108
+ if (!response.ok) {
109
+ const errorData = await response.json();
 
 
 
110
 
111
+ if (errorData.error === 'token_limit_exceeded') {
112
+ setIsStreaming(false);
113
+ setCurrentStreamId(null);
114
+ setTokenLimitReached(true);
115
+
116
+ setMessages(prev => [...prev.filter(m => m.id !== streamMessageId), {
117
+ sender: 'bot',
118
+ text: "⚠️ **Limite de taille de conversation atteinte**\n\nCette conversation est devenue trop longue. Pour continuer à discuter, veuillez créer une nouvelle conversation."
119
+ }]);
120
+
121
+ return;
122
+ }
123
 
124
+ throw new Error(`Chat API error ${response.status}`);
125
  }
126
 
127
+ if (response.headers.get('content-type')?.includes('text/event-stream') && response.body) {
128
+ const reader = response.body.getReader();
129
+ const decoder = new TextDecoder();
130
+ let fullText = '';
131
+
132
+ while (true) {
133
+ const { done, value } = await reader.read();
134
+ if (done) break;
135
+
136
+ const chunk = decoder.decode(value);
137
+ const lines = chunk.split('\n\n');
138
+
139
+ for (const line of lines) {
140
+ if (line.startsWith('data: ')) {
141
+ try {
142
+ const data = JSON.parse(line.slice(5));
143
+
144
+ if (data.type === 'start') {
145
+ console.log("Début du streaming");
146
+ fullText = '';
147
+ accumulatedText.current = '';
148
+ }
149
+ else if (data.type === 'end') {
150
+ console.log("SSE End received");
151
+ setIsStreaming(false);
152
+ setCurrentStreamId(null);
153
+ setIsLoading(false);
154
+
155
+ setMessages(prev =>
156
+ prev.map(msg =>
157
+ msg.id === streamMessageId
158
+ ? { ...msg, sender: 'bot', text: fullText }
159
+ : msg
160
+ )
161
+ );
162
+
163
+ if (typeof refreshConversationList === 'function') {
164
+ setTimeout(refreshConversationList, 100);
165
+ }
166
+
167
+ return;
168
+ }
169
+ else if (data.content) {
170
+ fullText += data.content;
171
+ accumulatedText.current += data.content;
172
+
173
+ if (accumulatedText.current.length >= updateThreshold || data.content.includes('\n')) {
174
+ setMessages(prev => {
175
+ const userMsg = prev.find(m => m.sender === 'user' && m.text === message);
176
+ const botMsg = prev.find(m => m.id === streamMessageId);
177
+
178
+ const updatedMessages = userMsg ? prev : [
179
+ ...prev,
180
+ { sender: 'user', text: message, id: `user-${Date.now()}` }
181
+ ];
182
+
183
+ if (botMsg) {
184
+ return updatedMessages.map(msg =>
185
+ msg.id === streamMessageId ? { ...msg, text: fullText } : msg
186
+ );
187
+ } else {
188
+ return [...updatedMessages, { sender: 'bot', text: fullText, id: streamMessageId }];
189
+ }
190
+ });
191
+
192
+ accumulatedText.current = ''; // Réinitialiser l'accumulateur
193
+
194
+ requestAnimationFrame(() => {
195
+ scrollToBottom();
196
+ });
197
+ }
198
+ }
199
+ else if (data.type === 'error') {
200
+ console.error("SSE Error received:", data.error);
201
+ setIsStreaming(false);
202
+ setCurrentStreamId(null);
203
+ setIsLoading(false);
204
+
205
+ setMessages(prev =>
206
+ prev.map(msg =>
207
+ msg.id === streamMessageId
208
+ ? { sender: 'bot', text: "Désolé, une erreur s'est produite." }
209
+ : msg
210
+ )
211
+ );
212
+ }
213
+ } catch (e) {
214
+ console.error('Error parsing SSE data:', e, line);
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ else {
221
+ console.log("Received non-streaming response.");
222
+ const responseData = await response.json();
223
+ setIsStreaming(false);
224
+ setCurrentStreamId(null);
225
+ setIsLoading(false);
226
+
227
+ setMessages(prev =>
228
+ prev.map(msg =>
229
+ msg.id === streamMessageId
230
+ ? { sender: 'bot', text: responseData.response, id: streamMessageId }
231
+ : msg
232
+ )
233
+ );
234
+
235
+ if (typeof refreshConversationList === 'function') {
236
+ setTimeout(refreshConversationList, 100);
237
+ }
238
  }
239
 
240
  } catch (error) {
241
+ console.error('Erreur lors de l\'envoi/réception du message:', error);
242
+ setIsStreaming(false);
243
+ setCurrentStreamId(null);
244
  setIsLoading(false);
245
+
246
+ setMessages(prev => {
247
+ const filteredMessages = prev.filter(m => m.id !== currentStreamId);
248
+ return [...filteredMessages,
249
+ { sender: 'bot', text: "Désolé, une erreur s'est produite. Veuillez réessayer." }
250
+ ];
251
+ });
252
  }
253
  };
254
 
 
256
  onNewChat();
257
  setTokenLimitReached(false);
258
  setHasInteractionStarted(false);
 
259
  };
260
+
261
  useEffect(() => {
262
  if (activeConversationId === null && messages.length === 0) {
263
  setHasInteractionStarted(false);
264
  }
265
  }, [activeConversationId, messages]);
266
 
 
 
267
  const handleSubmit = (e) => {
268
  e.preventDefault();
269
  const txt = inputMessage.trim();
 
273
  if (textareaRef.current) textareaRef.current.style.height = 'auto';
274
  };
275
 
276
+ const isMarkdown = (text, sender) => {
277
+ if (sender === 'bot') {
278
+ return true;
279
+ }
280
+ return /(?:\*\*|__|##|\*|_|`|>|\d+\.\s|\-\s|\[.*\]\(.*\))/.test(text);
281
+ };
282
+
283
+
284
  return (
285
  <div className="chat-container">
286
  {messages.length === 0 && !hasInteractionStarted ? (
 
333
  </div>
334
  <div className="messages-container">
335
  {messages.map((msg, index) => {
336
+ const isActiveStreaming = isStreaming && msg.id === currentStreamId;
337
+
338
+ return (
339
+ <div key={msg.id || index} className={`message ${msg.sender}`}>
340
+ <div className={`message-content ${isActiveStreaming ? 'streaming-message' : ''}`}>
341
+ {isMarkdown(msg.text, msg.sender) ?
342
+ <ReactMarkdown>{msg.text}</ReactMarkdown> :
343
+ <span>{msg.text}</span>
344
+ }
345
+ {isActiveStreaming && (
346
+ <span className="typing-indicator">▌</span>
347
+ )}
348
+ </div>
349
+ </div>
350
+ );
351
+ })}
352
+
353
+ {tokenLimitReached && (
354
  <div className="token-limit-warning">
355
  <button
356
  className="new-conversation-button"
 
360
  </button>
361
  </div>
362
  )}
 
 
 
 
 
 
 
363
 
364
  {isLoading && (
365
  <div className="message bot">
 
392
  }}
393
  />
394
  <button type="submit" disabled={isLoading || !inputMessage.trim() || tokenLimitReached}>
395
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3">
396
  <path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/>
397
  </svg>
398
  </button>
 
407
  );
408
  };
409
 
410
+ export default ChatInterface;