AxL95 commited on
Commit
df5c323
·
verified ·
1 Parent(s): d155f3f

Upload 16 files

Browse files
frontend/src/App.css ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ----- Disposition générale ----- */
2
+ .App {
3
+ display: flex;
4
+ min-height: 100vh;
5
+ margin: 0;
6
+ background-image: url('../public/blurBackground.png');
7
+ background-size: cover;
8
+ background-position: center;
9
+ background-repeat: no-repeat;
10
+ background-attachment: fixed;
11
+ }
12
+ .body{
13
+ background-image: url('../public/blurBackground.png');
14
+ background-size: cover;
15
+ background-position: center;
16
+ background-repeat: no-repeat;
17
+ background-attachment: fixed;
18
+ }
19
+
20
+ /* ----- Panel latéral (sidebar) ----- */
21
+ .sidebar-panel {
22
+ display: flex;
23
+ flex-direction: column;
24
+ width: 100%;
25
+ max-width: 250px;
26
+ height: 100vh; /* Add fixed height */
27
+ background-color: #1E2136;
28
+ color: #ffffff;
29
+ overflow-y: auto;
30
+ transition: max-width 0.3s ease;
31
+ }
32
+
33
+ .App.panel-collapsed .main-content {
34
+ margin-left: 0;
35
+ }
36
+
37
+ .sidebar-panel.collapsed {
38
+ max-width: 0;
39
+ overflow: hidden;
40
+ }
41
+
42
+ /* ----- Contenu principal (zone de chat + header) ----- */
43
+ .main-content {
44
+ flex: 1;
45
+ display: flex;
46
+ flex-direction: column;
47
+ transition: margin-left 0.3s ease;
48
+
49
+ }
50
+ .new-chat-button {
51
+
52
+ background-color: #1e2136;
53
+ border: none;
54
+ border-radius: 50%;
55
+
56
+ cursor: pointer;
57
+ }
58
+
59
+ .new-chat-button:hover {
60
+ background-color: #2a2d45;
61
+ }
62
+
63
+ .new-chat-button svg {
64
+ width: 20px;
65
+ height: 20px;
66
+ fill: #ffffff;
67
+ }
68
+ /* ----- Header du chat ----- */
69
+ .chat-header {
70
+ display: flex;
71
+ position: relative;
72
+ justify-content: center;
73
+ align-items: center;
74
+ padding: 10px 15px;
75
+ color: #ffffff;
76
+ font-size: 1.3rem;
77
+ margin: 0;
78
+ }
79
+ .chat-header .avatar-container {
80
+ position: absolute;
81
+ right: 15px;
82
+ top: 50%;
83
+ transform: translateY(-50%);
84
+ }
85
+
86
+ .chat-title {
87
+ font-weight: 400;
88
+ font-size: 1.3rem;
89
+ color: #ffffff;
90
+ text-transform: uppercase;
91
+ letter-spacing: 1px;
92
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
93
+ font-family: 'Montserrat', sans-serif;
94
+ text-align: center;
95
+ }
96
+ /* ----- Conteneur du chat (zone de messages + input) ----- */
97
+ .chat-container {
98
+ display: flex;
99
+ flex-direction: column;
100
+ height: 100vh;
101
+ }
102
+ /* ----- Vue sans message : contenu centré ----- */
103
+ .no-messages-view {
104
+ flex: 1;
105
+ display: flex;
106
+ flex-direction: column;
107
+ justify-content: center;
108
+ align-items: center;
109
+ padding: 20px;
110
+ text-align: center;
111
+ }
112
+ .no-messages-view .welcome-message {
113
+ margin-bottom: 20px;
114
+ color:white;
115
+ font-size:1.5rem;
116
+ }
117
+
118
+ .no-messages-view .input-container {
119
+ position: relative;
120
+ bottom: auto;
121
+ left: auto;
122
+ right: auto;
123
+ margin-top: 20px;
124
+ width: 100%;
125
+ }
126
+
127
+
128
+ .welcome-content {
129
+ flex-direction: column;
130
+ align-items: center;
131
+ justify-content: center;
132
+ max-width: 600px;
133
+ width: 100%;
134
+ margin-bottom: 100px;
135
+ }
136
+
137
+ .no-messages-view .input-form {
138
+ max-width: 600px;
139
+ width: 100%;
140
+ }
141
+
142
+ /* ----- Zone des messages scrollable ----- */
143
+ .messages-container {
144
+ flex: 1;
145
+ overflow-y: auto;
146
+ padding: 20px 15px;
147
+ display: flex;
148
+ flex-direction: column;
149
+ align-items: center;
150
+ justify-content: flex-start;
151
+ gap: 12px;
152
+ margin-left: 10vw;
153
+ margin-right: 10vw;
154
+ margin-bottom: 8vw;
155
+ }
156
+ .messages-container::-webkit-scrollbar {
157
+
158
+ display:none;
159
+
160
+ }
161
+
162
+ @media (max-width: 768px) {
163
+ .messages-container {
164
+ margin-left: 5vw;
165
+ margin-right: 5vw;
166
+ margin-bottom: 100px;
167
+ }
168
+ }
169
+
170
+ @media (max-width: 480px) {
171
+ .messages-container {
172
+ margin-left: 2vw;
173
+ margin-right: 2vw;
174
+ margin-bottom: 120px; /* Ajuste l'espace en bas pour les très petits écrans */
175
+ }
176
+ }
177
+ /* ----- Style des messages ----- */
178
+ .message {
179
+ max-width: 70%; /* Limite la largeur des messages */
180
+ padding: 12px 16px;
181
+ border-radius: 8px;
182
+ word-break: break-word;
183
+ animation: fade-in 0.3s ease-in-out;
184
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); /* Ajoute une ombre légère */
185
+ }
186
+ .message.user {
187
+ align-self: flex-end; /* Aligne à droite */
188
+ background: rgba(67, 72, 105, 0.3);
189
+ backdrop-filter: blur(10px);
190
+ color: #ffffff;
191
+ font-size: 1rem;
192
+ line-height: 1.5;
193
+ border-bottom-right-radius: 4px;
194
+ }
195
+
196
+ .message.bot .message-content{
197
+ display:flex;
198
+ align-self: flex-end;
199
+ }
200
+ .message.user .message-content{
201
+ display:flex;
202
+ align-self: flex-start;
203
+ }
204
+ .message.bot {
205
+ align-self: flex-start;
206
+ color: #e1e1e1;
207
+ font-size: 1rem;
208
+ line-height: 1.5;
209
+ border-bottom-left-radius: 4px;
210
+ }
211
+
212
+ /* ----- Animation pour l'apparition ----- */
213
+ @keyframes fade-in {
214
+ from { opacity: 0; transform: translateY(5px); }
215
+ to { opacity: 1; transform: translateY(0); }
216
+ }
217
+
218
+ /* ----- Indicateur de chargement ----- */
219
+ .loading span {
220
+ display: inline-block;
221
+ animation: loading-dots 1.4s infinite ease-in-out;
222
+ font-size: 1.2rem;
223
+ margin-right: 2px;
224
+ }
225
+
226
+ @keyframes loading-dots {
227
+ 0%, 80%, 100% { transform: scale(0); }
228
+ 40% { transform: scale(1); }
229
+ }
230
+
231
+ .input-container {
232
+ position: fixed;
233
+ bottom: 20px;
234
+ left: 0;
235
+ right: 0;
236
+ display: flex;
237
+ flex-direction: column;
238
+ align-items: center;
239
+ z-index: 999;
240
+ width: 100%;
241
+ }
242
+ .disclaimer-text {
243
+ font-size: 12px;
244
+ color: #666;
245
+ text-align: center;
246
+ margin-top: 8px;
247
+ font-style: italic;
248
+ max-width: 600px;
249
+ }
250
+
251
+ .conversation-date{
252
+ font-size: 0.8rem;
253
+ font-style: italic;
254
+ margin-top: 2px;
255
+ color:rgb(183, 183, 183);
256
+ }
257
+
258
+ /* ----- Formulaire d'entrée ----- */
259
+ .input-form {
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: space-between;
263
+ background: linear-gradient(180deg, #1E2136 0%, #1E2136 100%);
264
+ border-radius: 20px;
265
+ padding: 12px 16px;
266
+ width: 100%;
267
+ max-width: 600px;
268
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
269
+ }
270
+ /* Input */
271
+ .input-form input {
272
+ flex: 1;
273
+ padding: 10px 12px;
274
+ border: none;
275
+ border-radius: 16px;
276
+ font-size: 14px;
277
+ outline: none;
278
+ background: transparent;
279
+ color: rgba(255, 255, 255, 0.8);
280
+ }
281
+
282
+ .input-form textarea {
283
+ flex: 1;
284
+ padding: 10px 12px;
285
+ border: none;
286
+ border-radius: 16px;
287
+ font-size: 14px;
288
+ outline: none;
289
+ background: transparent;
290
+ color: rgba(255, 255, 255, 0.8);
291
+ resize: none;
292
+ overflow-y: auto;
293
+ height: auto;
294
+ max-height: 150px;
295
+
296
+ }
297
+ .input-form textarea::placeholder {
298
+ color: rgba(255, 255, 255, 0.5);
299
+ }
300
+ textarea::-webkit-scrollbar {
301
+ width: 12px;
302
+ }
303
+
304
+ /* For Firefox */
305
+ textarea {
306
+ scrollbar-width: thin;
307
+ scrollbar-color: #acacac transparent;
308
+ }
309
+
310
+ textarea:hover {
311
+ scrollbar-color: #b1b1b1 transparent;
312
+ }
313
+
314
+ /* Placeholder */
315
+ .input-form input::placeholder {
316
+ color: rgba(255, 255, 255, 0.5);
317
+ }
318
+
319
+ /* Bouton d'envoi */
320
+ .input-form button {
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ background: transparent;
325
+ border: none;
326
+ cursor: pointer;
327
+ color: rgba(255, 255, 255, 0.8);
328
+ font-size: 16px;
329
+ padding: 6px 12px;
330
+ margin-left: 8px;
331
+ transition: opacity 0.2s;
332
+ border-radius: 50%;
333
+
334
+ }
335
+
336
+ .input-form button:hover {
337
+ opacity: 0.7;
338
+ }
339
+
340
+ /* Désactivation du bouton */
341
+ .input-form button:disabled {
342
+ opacity: 0.3;
343
+ cursor: not-allowed;
344
+ }
345
+
346
+ h2{
347
+ display: block;
348
+ text-align: center;
349
+ }
350
+ /* ----- Bouton de collapse principal ----- */
351
+ .collapse-button-main {
352
+ position: absolute;
353
+ top: 20px;
354
+ left: 20px;
355
+ z-index: 999;
356
+ background: none;
357
+ border: none;
358
+ color: #fff;
359
+ cursor: pointer;
360
+ font-size: 24px;
361
+ }
362
+
363
+ /* ----- Icônes ----- */
364
+ .material-icons {
365
+ vertical-align: middle;
366
+ }
367
+
368
+ /* ----- Liste des conversations dans le panel ----- */
369
+ .conversations-list {
370
+ display: flex;
371
+ flex-direction: column;
372
+ flex: 1;
373
+ overflow-y: auto;
374
+ max-height: calc(100vh - 100px);
375
+ }
376
+
377
+
378
+ .conversations-list::-webkit-scrollbar {
379
+ width: 12px;
380
+ }
381
+
382
+ .conversation-today-title{
383
+ display: flex;
384
+ margin-left: 6%;
385
+ }
386
+ .conversation-before-title{
387
+ display: flex;
388
+ margin-left: 6%;
389
+ }
390
+ .delete-button{
391
+ background: none;
392
+ border: none;
393
+ color: #fff;
394
+ cursor: pointer;
395
+ font-size: 24px;
396
+ margin-left: auto;
397
+
398
+ }
399
+ /* For Firefox */
400
+ .conversations-list {
401
+ scrollbar-width: thin;
402
+ scrollbar-color: #acacac transparent;
403
+ }
404
+
405
+ .conversations-list:hover {
406
+ scrollbar-color: #b1b1b1 transparent;
407
+ }
408
+
409
+ .conversation-item {
410
+ padding: 8px;
411
+ gap: 12px;
412
+ border-bottom: 1px solid #525467;
413
+ cursor: pointer;
414
+ display: flex;
415
+ align-items: center;
416
+ }
417
+
418
+ .conversation-item:hover {
419
+ background-color: #525467;
420
+ }
421
+
422
+ .conversation-icon {
423
+ margin-right: 6px;
424
+ }
425
+
426
+ /* ----- Footer du panel ----- */
427
+ .sidebar-footer {
428
+ margin-top: auto;
429
+ border-top: 1px solid #525467;
430
+ padding: 8px;
431
+ }
432
+ .sidebar-header{
433
+ display:flex;
434
+ justify-content: space-between;
435
+ align-items: center;
436
+ margin-bottom: 1.3rem;
437
+ padding-top:0.5rem;
438
+ padding-inline-start: calc(0.5rem);
439
+ padding-inline-end: calc(0.5rem); ;}
440
+
441
+ .user-info {
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 6px;
445
+ }
446
+
447
+ .user-avatar {
448
+ font-size: 20px;
449
+ }
450
+
451
+ .collapse-button {
452
+ background: none;
453
+ border: none;
454
+ color: #fff;
455
+ cursor: pointer;
456
+ }
frontend/src/App.js ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import './App.css';
3
+ import ChatInterface from './components/ChatInterface';
4
+ import Panel from './components/Panel';
5
+ import Login from './components/Login';
6
+ import Signin from './components/Signin';
7
+
8
+
9
+ function App() {
10
+ const [isCollapsed, setIsCollapsed] = useState(false);
11
+ const [messages, setMessages] = useState([]);
12
+ const [conversations, setConversations] = useState([
13
+ { id: 1, title: "Premier diagnostic", date: "12 Mar" },
14
+ { id: 2, title: "Question sur les symptômes", date: "11 Mar" },
15
+ { id: 3, title: "Consultation générale", date: "10 Mar" }
16
+ ]);
17
+ const [activeConversationId, setActiveConversationId] = useState(null);
18
+
19
+ const toggleCollapse = () => {
20
+ setIsCollapsed(!isCollapsed);
21
+ };
22
+
23
+ const handleNewChat = () => {
24
+ setActiveConversationId(null);
25
+ setMessages([]);
26
+ };
27
+
28
+ const handleMessageSent = (message) => {
29
+
30
+ if (!activeConversationId) {
31
+ const newChat = {
32
+ id: Date.now(),
33
+ title: message.length > 15 ? message.substring(0, 15) + "..." : message,
34
+ date: new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: 'short' }),
35
+ time: new Date().toLocaleTimeString('fr-FR', { hour: 'numeric', minute: 'numeric' })
36
+
37
+ };
38
+ setConversations([newChat, ...conversations]);
39
+ setActiveConversationId(newChat.id);
40
+ }
41
+ };
42
+
43
+ const [page, setPage] = useState("chat");
44
+
45
+ return (
46
+ <div className={`App ${isCollapsed ? 'panel-collapsed' : ''}`}>
47
+
48
+ {page === "chat" && (
49
+ <Panel
50
+ conversations={conversations}
51
+ setConversations={setConversations}
52
+ activeConversationId={activeConversationId}
53
+ setActiveConversationId={setActiveConversationId}
54
+ onNewChat={handleNewChat}
55
+ onToggleCollapse={toggleCollapse}
56
+ isCollapsed={isCollapsed}
57
+ />
58
+ )}
59
+
60
+ <div className="main-content">
61
+ {isCollapsed && (
62
+ <button className="collapse-button-main" onClick={toggleCollapse}>
63
+ <span className="material-icons">
64
+ <svg
65
+ fill="#FFFF"
66
+ width="20"
67
+ height="20"
68
+ viewBox="0 0 32 32"
69
+ xmlns="http://www.w3.org/2000/svg"
70
+ >
71
+ <defs>
72
+ <style>{`.cls-1{fill:none;}`}</style>
73
+ </defs>
74
+ <title>open-panel--solid--left</title>
75
+ <path d="M28,4H4A2,2,0,0,0,2,6V26a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V6A2,2,0,0,0,28,4Zm0,22H12V6H28Z" />
76
+ <rect
77
+ id="_Transparent_Rectangle_"
78
+ data-name="<Transparent Rectangle>"
79
+ className="cls-1"
80
+ width="20"
81
+ height="20"
82
+ />
83
+ </svg>
84
+ </span>
85
+ </button>
86
+ )}
87
+
88
+ {page === "chat" &&
89
+ <ChatInterface
90
+ messages={messages}
91
+ setMessages={setMessages}
92
+ onMessageSent={handleMessageSent}
93
+ toLogin={() => setPage("login")}/>}
94
+ {page === "login" && <Login toSignin={() => setPage("signin")}/>}
95
+ {page === "signin" && <Signin toLogin={() => setPage("login")}/>}
96
+ </div>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ export default App;
frontend/src/App.test.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { render, screen } from '@testing-library/react';
2
+ import App from './App';
3
+
4
+ test('renders learn react link', () => {
5
+ render(<App />);
6
+ const linkElement = screen.getByText(/learn react/i);
7
+ expect(linkElement).toBeInTheDocument();
8
+ });
frontend/src/avatar.css ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .avatar-container {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ }
6
+
7
+ .avatar-image-wrapper {
8
+ width: 2rem;
9
+ height: 2rem;
10
+ overflow: hidden;
11
+ border-radius: 50%;
12
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
13
+ cursor: pointer;
14
+ transition: transform 0.3s ease;
15
+ }
16
+
17
+
18
+
19
+ .circular--portrait-img {
20
+ width: 100%;
21
+ height: 100%;
22
+ object-fit: cover;
23
+ }
frontend/src/components/AddUser.jsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+
3
+ const AddUser = () => {
4
+ const [name, setName] = useState('');
5
+ const [email, setEmail] = useState('');
6
+ const [message, setMessage] = useState('');
7
+
8
+ const handleSubmit = async (e) => {
9
+ e.preventDefault();
10
+ try {
11
+ const response = await fetch('http://localhost:5001/add_user', {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ body: JSON.stringify({ name, email }),
17
+ });
18
+ const data = await response.json();
19
+ if (response.ok) {
20
+ setMessage(data.message);
21
+ } else {
22
+ setMessage(data.error);
23
+ }
24
+ } catch (error) {
25
+ setMessage('An error occurred');
26
+ }
27
+ };
28
+
29
+ return (
30
+ <div>
31
+ <h1>Add User</h1>
32
+ <form onSubmit={handleSubmit}>
33
+ <div>
34
+ <label>Name:</label>
35
+ <input
36
+ type="text"
37
+ value={name}
38
+ onChange={(e) => setName(e.target.value)}
39
+ required
40
+ />
41
+ </div>
42
+ <div>
43
+ <label>Email:</label>
44
+ <input
45
+ type="email"
46
+ value={email}
47
+ onChange={(e) => setEmail(e.target.value)}
48
+ required
49
+ />
50
+ </div>
51
+ <button type="submit">Add User</button>
52
+ </form>
53
+ {message && <p>{message}</p>}
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default AddUser;
frontend/src/components/Avatar.jsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import '../avatar.css';
3
+
4
+ export default function Avatar({onClick}) {
5
+
6
+
7
+ return (
8
+ <div className="avatar-container" onClick={onClick}>
9
+ <div
10
+ className="avatar-image-wrapper"
11
+
12
+ >
13
+ <img
14
+ className="circular--portrait-img"
15
+ alt="me"
16
+ src={new URL("../../public/emojipouce.png", import.meta.url).href}
17
+ />
18
+ </div>
19
+ </div>
20
+ );
21
+ }
frontend/src/components/ChatInterface.jsx ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import axios from 'axios';
3
+ import ReactMarkdown from 'react-markdown';
4
+ import Avatar from './Avatar.jsx';
5
+ import '../App.css';
6
+
7
+ const ChatInterface = ({ messages = [], setMessages = () => {}, onMessageSent = () => {}, toLogin }) => {
8
+ const [inputMessage, setInputMessage] = useState('');
9
+ const [isLoading, setIsLoading] = useState(false);
10
+ const messagesEndRef = useRef(null);
11
+ const textareaRef = useRef(null);
12
+
13
+
14
+ const isMarkdown = (text) => /[#*_>`-]/.test(text);
15
+
16
+ const scrollToBottom = () => {
17
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
18
+ };
19
+
20
+
21
+ useEffect(() => {
22
+ scrollToBottom();
23
+ }, [messages]);
24
+
25
+ const sendMessage = async (message) => {
26
+ try {
27
+ setIsLoading(true);
28
+
29
+ onMessageSent(message);
30
+
31
+ const response = await axios.post('https://15af0837fca124cf6d.gradio.live/gradio_api/run/predict', {
32
+ data: [message, null],
33
+ fn_index: 0
34
+ });
35
+ const botResponse = response.data.data[0];
36
+ setMessages(prev => [
37
+ ...prev,
38
+ { sender: 'user', text: message },
39
+ { sender: 'bot', text: botResponse }
40
+ ]);
41
+ setIsLoading(false);
42
+ } catch (error) {
43
+ console.error("Erreur:", error);
44
+ setIsLoading(false);
45
+ setMessages(prev => [
46
+ ...prev,
47
+ { sender: 'user', text: message },
48
+ { sender: 'bot', text: "Désolé, une erreur s'est produite. Veuillez réessayer. 👍" }
49
+ ]);
50
+ }
51
+ };
52
+
53
+ const handleSubmit = (e) => {
54
+ e.preventDefault();
55
+ if (inputMessage.trim() === '') return;
56
+ sendMessage(inputMessage);
57
+ if (textareaRef.current) {
58
+ textareaRef.current.style.height = 'auto';
59
+ }
60
+ setInputMessage('');
61
+
62
+ };
63
+
64
+ // Le reste du composant reste inchangé
65
+ return (
66
+ <div className="chat-container">
67
+ {messages.length === 0 ? (
68
+ <>
69
+ <div className="chat-header">
70
+ <h2 className="chat-title">Medic.ial</h2>
71
+ <Avatar onClick={toLogin}/>
72
+ </div>
73
+ <div className="no-messages-view">
74
+ <div className="welcome-content">
75
+ <div className="welcome-message">
76
+ <p>Bonjour ! Comment puis-je vous aider aujourd'hui ? 🧑‍⚕️</p>
77
+ </div>
78
+ <div className="input-container centered">
79
+ <form onSubmit={handleSubmit} className="input-form">
80
+ <textarea
81
+ value={inputMessage}
82
+ onChange={(e) => setInputMessage(e.target.value)}
83
+ placeholder="Posez une question..."
84
+ disabled={isLoading}
85
+ rows="1"
86
+ className="input-textarea"
87
+ onKeyDown={(e) => {
88
+ if (e.key === "Enter" && !e.shiftKey) {
89
+ e.preventDefault();
90
+ handleSubmit(e);
91
+ }
92
+ }}
93
+ onInput={(e) => {
94
+ e.target.style.height = "auto"; // Réinitialise la hauteur
95
+ e.target.style.height = `${e.target.scrollHeight}px`; // Ajuste à la hauteur du contenu
96
+ }}
97
+ />
98
+ <button type="submit" style={{background:"none"}} disabled={isLoading || inputMessage.trim() === ''}>
99
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3">
100
+ <path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/>
101
+ </svg>
102
+ </button>
103
+ </form>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </>
108
+
109
+ ) : (
110
+ <>
111
+ <div className="chat-header">
112
+ <Avatar onClick={toLogin}/>
113
+ <h2 className="chat-title">Medic.ial</h2>
114
+ </div>
115
+ <div className="messages-container">
116
+ {messages.map((msg, index) => (
117
+ <div key={index} className={`message ${msg.sender}`}>
118
+ <div className="message-content">
119
+ {isMarkdown(msg.text) ? (
120
+ <ReactMarkdown>{msg.text}</ReactMarkdown>
121
+ ) : (
122
+ msg.text
123
+ )}
124
+ </div>
125
+ </div>
126
+ ))}
127
+ {isLoading && (
128
+ <div className="message bot">
129
+ <div className="message-content loading">
130
+ <span>.</span><span>.</span><span>.</span>
131
+ </div>
132
+ </div>
133
+ )}
134
+ <div ref={messagesEndRef} />
135
+ </div>
136
+ <div className="input-container">
137
+ <form onSubmit={handleSubmit} className="input-form">
138
+ <textarea
139
+ value={inputMessage}
140
+ onChange={(e) => setInputMessage(e.target.value)
141
+ }
142
+ placeholder="Tapez votre message ici..."
143
+ disabled={isLoading}
144
+ rows="1"
145
+ ref={textareaRef}
146
+ className="input-textarea"
147
+ onKeyDown={(e) => {
148
+ if (e.key === "Enter" && !e.shiftKey) {
149
+ e.preventDefault();
150
+ handleSubmit(e);
151
+ }
152
+ }}
153
+ onInput={(e) => {
154
+ e.target.style.height = "auto";
155
+ e.target.style.height = `${e.target.scrollHeight}px`;
156
+ }}
157
+ />
158
+ <button type="submit" disabled={isLoading || inputMessage.trim() === ''}>
159
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3">
160
+ <path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/>
161
+ </svg>
162
+ </button>
163
+ </form>
164
+ <figcaption className="disclaimer-text">Medic.ial est sujet à faire des erreurs. Vérifiez les informations fournies.</figcaption>
165
+ </div>
166
+ </>
167
+ )}
168
+ </div>
169
+ );
170
+ };
171
+ /*const UserList = () => {
172
+ const [users, setUsers] = useState([]);
173
+
174
+ useEffect(() => {
175
+ const fetchUsers = async () => {
176
+ try {
177
+ const response = await fetch('/.netlify/functions/get_users');
178
+ const data = await response.json();
179
+ setUsers(data);
180
+ } catch (error) {
181
+ console.error('An error occurred while fetching users:', error);
182
+ }
183
+ };
184
+
185
+ fetchUsers();
186
+ }, []);
187
+
188
+ return (
189
+ <div>
190
+ <h1>User List</h1>
191
+ <ul>
192
+ {users.map((user, index) => (
193
+ <li key={index}>{user[1]} - {user[2]}</li>
194
+ ))}
195
+ </ul>
196
+ </div>
197
+ );
198
+ };*/
199
+
200
+ export default ChatInterface;
frontend/src/components/Login.jsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import '../login.css';
3
+
4
+
5
+ const Login = ({toSignin}) => {
6
+
7
+
8
+
9
+ return (
10
+ <div className='login'>
11
+ <h1 className='title-1'>Te voilà de retour sur Medic.ial !</h1>
12
+ <div className="container">
13
+ <div className='container-form'>
14
+ <form action="" className='form'>
15
+ <h3>CONNEXION</h3>
16
+ <div className='form-container-input'>
17
+ <p className='form-title'>Adresse email</p>
18
+ <input type='mail' className='form-input'/>
19
+ </div>
20
+ <div className='form-container-input'>
21
+ <p className='form-title'>Mot de passe</p>
22
+ <input type='password' className='form-input'/>
23
+ </div>
24
+ <div className='form-container-submit'>
25
+ <button type="submit">Connexion</button>
26
+ <p>Mot de passe oublié?</p>
27
+ </div>
28
+ </form>
29
+ </div>
30
+ </div>
31
+ <p className='title-2'>Pas encore de compte ? <span onClick={toSignin}>S'inscrire</span></p>
32
+ </div>
33
+ )
34
+ }
35
+
36
+ export default Login
frontend/src/components/Panel.jsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import '../App.css';
3
+
4
+ const Panel = ({
5
+ conversations = [],
6
+ setConversations = () => {},
7
+ activeConversationId,
8
+ setActiveConversationId,
9
+ onToggleCollapse,
10
+ isCollapsed,
11
+ onNewChat
12
+ }) => {
13
+
14
+
15
+ const createNewChat = () => {
16
+ onNewChat();
17
+ };
18
+
19
+ const deleteConversation=(conversationId)=>{
20
+ console.log(conversationId)
21
+ setConversations(prev => prev.filter(chat => chat.id !== conversationId));
22
+ setActiveConversationId(null);
23
+
24
+ }
25
+
26
+ return (
27
+ <div className={`sidebar-panel ${isCollapsed ? 'collapsed' : ''}`}>
28
+ <div className="sidebar-header">
29
+ {!isCollapsed && (
30
+ <button className="collapse-button" onClick={onToggleCollapse}>
31
+ <svg
32
+ fill="#FFFF"
33
+ width="20"
34
+ height="20"
35
+ viewBox="0 0 32 32"
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ >
38
+ <defs>
39
+ <style>{`.cls-1{fill:none;}`}</style>
40
+ </defs>
41
+ <title>open-panel--solid--left</title>
42
+ <path d="M28,4H4A2,2,0,0,0,2,6V26a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V6A2,2,0,0,0,28,4Zm0,22H12V6H28Z" />
43
+ <rect
44
+ id="_Transparent_Rectangle_"
45
+ data-name="<Transparent Rectangle>"
46
+ className="cls-1"
47
+ width="20"
48
+ height="20"
49
+ />
50
+ </svg>
51
+ </button>
52
+ )}
53
+ <button className="new-chat-button" onClick={createNewChat}>
54
+ {!isCollapsed && (
55
+ <span>
56
+ <svg
57
+ viewBox="0 0 512 512"
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ width="24"
60
+ height="24"
61
+ fill="currentColor"
62
+ >
63
+ <path d="M495.6 49.23l-32.82-32.82C451.8 5.471 437.5 0 423.1 0c-14.33 0-28.66 5.469-39.6 16.41L167.5 232.5C159.1 240 154.8 249.5 152.4 259.8L128.3 367.2C126.5 376.1 133.4 384 141.1 384c.916 0 1.852-.0918 2.797-.2813c0 0 74.03-15.71 107.4-23.56c10.1-2.377 19.13-7.459 26.46-14.79l217-217C517.5 106.5 517.4 71.1 495.6 49.23zM461.7 94.4L244.7 311.4C243.6 312.5 242.5 313.1 241.2 313.4c-13.7 3.227-34.65 7.857-54.3 12.14l12.41-55.2C199.6 268.9 200.3 267.5 201.4 266.5l216.1-216.1C419.4 48.41 421.6 48 423.1 48s3.715 .4062 5.65 2.342l32.82 32.83C464.8 86.34 464.8 91.27 461.7 94.4zM424 288c-13.25 0-24 10.75-24 24v128c0 13.23-10.78 24-24 24h-304c-13.22 0-24-10.77-24-24v-304c0-13.23 10.78-24 24-24h144c13.25 0 24-10.75 24-24S229.3 64 216 64L71.1 63.99C32.31 63.99 0 96.29 0 135.1v304C0 479.7 32.31 512 71.1 512h303.1c39.69 0 71.1-32.3 71.1-72L448 312C448 298.8 437.3 288 424 288z" />
64
+ </svg>
65
+ </span>
66
+ )}
67
+ </button>
68
+
69
+ </div>
70
+
71
+ <div className="conversations-list">
72
+ <div class="conversation-today">
73
+ <h6 className="conversation-today-title">Aujourd'hui</h6>
74
+ {conversations.map(chat => (
75
+ <div
76
+ key={chat.id}
77
+ className={`conversation-item ${activeConversationId === chat.id ? 'active' : ''}`}
78
+ onClick={() => setActiveConversationId(chat.id)}
79
+ >
80
+
81
+ {!isCollapsed && (
82
+ <>
83
+ <div className="conversation-icon">
84
+ <span className="material-icons">{chat.time}</span>
85
+ </div>
86
+ <div className="conversation-details">
87
+
88
+ <div className="conversation-title">{chat.title}</div>
89
+ <div className="conversation-date">{chat.date}</div>
90
+ </div>
91
+ <button className="delete-button" onClick={(e) => {
92
+ e.stopPropagation();
93
+ deleteConversation(chat.id);
94
+ }}>
95
+
96
+
97
+
98
+ <span>
99
+ <svg width="15px" height="15px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ff0000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M10 12V17" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M14 12V17" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M4 7H20" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M6 10V18C6 19.6569 7.34315 21 9 21H15C16.6569 21 18 19.6569 18 18V10" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
100
+ </span>
101
+
102
+ </button>
103
+ </>
104
+ )}
105
+ </div>
106
+ ))}
107
+ </div>
108
+ <div class="conversation-before">
109
+ <h6 className="conversation-before-title">Les 30 derniers jours</h6>
110
+
111
+ </div>
112
+ </div>
113
+
114
+ </div>
115
+ );
116
+ };
117
+
118
+ export default Panel;
frontend/src/components/Signin.jsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+ import '../login.css';
3
+
4
+ function Signin({toLogin}) {
5
+ const [formData, setFormData] = useState({
6
+ prenom: '',
7
+ nom: '',
8
+ email: '',
9
+ password: '', // Changé de password_hash à password pour correspondre à l'API
10
+ });
11
+
12
+ const handleChange = (e) => {
13
+ setFormData({...formData, [e.target.name]: e.target.value});
14
+ };
15
+
16
+ const handleSubmit = async (e) => {
17
+ e.preventDefault();
18
+ try {
19
+ const res = await fetch("/.netlify/functions/register", {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json"
23
+ },
24
+ body: JSON.stringify(formData)
25
+ });
26
+
27
+ if (res.ok) {
28
+ const data = await res.json();
29
+ console.log(data);
30
+ toLogin();
31
+ } else {
32
+ const errorData = await res.json().catch(() => ({}));
33
+ alert(errorData.detail || "Erreur d'inscription");
34
+ }
35
+ } catch (error) {
36
+ console.error("Erreur lors de l'inscription", error);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className='login'>
42
+ <h1 className='title-1'>Bienvenue sur Medic.ial !</h1>
43
+ <div className="container">
44
+ <div className='container-form'>
45
+ <form action="" className='form' onSubmit={handleSubmit}>
46
+ <h3>INSCRIPTION</h3>
47
+ <div className='form-container-name'>
48
+ <div>
49
+ <p className='form-title'>Prénom</p>
50
+ <input type='text' name="prenom" onChange={handleChange} value={formData.prenom} className='form-input'/>
51
+ </div>
52
+ <div>
53
+ <p className='form-title'>Nom</p>
54
+ <input type='text' name="nom" onChange={handleChange} value={formData.nom} className='form-input'/>
55
+ </div>
56
+ </div>
57
+ <div className='form-container-input'>
58
+ <p className='form-title'>Adresse email</p>
59
+ <input type='email' name="email" onChange={handleChange} value={formData.email} className='form-input'/>
60
+ </div>
61
+ <div className='form-container-input'>
62
+ <p className='form-title'>Mot de passe</p>
63
+ <input type='password' name="password" onChange={handleChange} value={formData.password} className='form-input'/>
64
+ </div>
65
+ <div className='form-container-submit'>
66
+ <button type="submit">Inscription</button>
67
+ </div>
68
+ </form>
69
+ </div>
70
+ </div>
71
+ <p className='title-2'>Déjà un compte ? <span onClick={toLogin}>Se connecter</span></p>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ export default Signin
frontend/src/index.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
+ sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ }
9
+
10
+ code {
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
+ monospace;
13
+ }
frontend/src/index.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App';
5
+ import reportWebVitals from './reportWebVitals';
6
+
7
+ const root = ReactDOM.createRoot(document.getElementById('root'));
8
+ root.render(
9
+ <React.StrictMode>
10
+ <App />
11
+ </React.StrictMode>
12
+ );
13
+
14
+ // If you want to start measuring performance in your app, pass a function
15
+ // to log results (for example: reportWebVitals(console.log))
16
+ // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17
+ reportWebVitals();
frontend/src/login.css ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Trade+Winds&display=swap');
2
+
3
+ .login {
4
+ width: 100%;
5
+ height: 100%;
6
+ margin: 0;
7
+ padding: 0;
8
+ color: white;
9
+ font-family: 'Montserrat', sans-serif;
10
+ }
11
+
12
+
13
+ .title-1 {
14
+ text-align: center;
15
+ font-weight: 200;
16
+ margin: 3%;
17
+ }
18
+
19
+ .title-2 {
20
+ text-align: center;
21
+ }
22
+
23
+ .title-2 span {
24
+ text-decoration: underline;
25
+ cursor: pointer;
26
+ }
27
+
28
+ .container {
29
+ display: flex;
30
+ justify-content: center;
31
+ align-items: center;
32
+ flex-direction: column;
33
+ font-family: 'Montserrat', sans-serif;
34
+ }
35
+
36
+ .container-form {
37
+ background-color: rgb(30, 33, 54, 0.2);
38
+ border-radius: 20px;
39
+ width: 40%;
40
+ height: 30rem;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ }
45
+
46
+ .form {
47
+ display: flex;
48
+ justify-content: space-around;
49
+ flex-direction: column;
50
+ color: white;
51
+ width: 60%;
52
+ height: 90%;
53
+ }
54
+
55
+
56
+ .form-container-input {
57
+ display: flex;
58
+ justify-content: center;
59
+ flex-direction: column;
60
+ }
61
+
62
+ .form-container-name {
63
+ display: flex;
64
+ justify-content: space-between;
65
+ flex-direction: row;
66
+
67
+ }
68
+
69
+ .form-container-input>input, .form-container-name input {
70
+ width: 100%;
71
+ height: 2rem;
72
+ color: white;
73
+ }
74
+
75
+ .form-container-name div {
76
+ width: 45%;
77
+ }
78
+
79
+ .form-input {
80
+ border: none;
81
+ box-shadow: none;
82
+ background-color: transparent;
83
+ border-bottom: 1px solid white;
84
+ }
85
+
86
+ .form-title {
87
+ font-size: x-small;
88
+ }
89
+
90
+ .form-container-submit {
91
+ width: 100%;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ flex-direction: column;
96
+ }
97
+
98
+ .form-container-submit>p {
99
+ text-decoration: underline;
100
+ font-size: small;
101
+ cursor: pointer;
102
+ }
103
+
104
+ .form-container-submit>button {
105
+ background: none;
106
+ border: 1px solid white;
107
+ border-radius: 10px;
108
+ color: white;
109
+ text-align: center;
110
+ width: 15rem;
111
+ height: 3rem;
112
+ font-family: 'Montserrat', sans-serif;
113
+ cursor: pointer;
114
+ }
115
+
116
+ .form-container-submit>button:hover {
117
+ background: white;
118
+ color: black;
119
+ }
frontend/src/logo.svg ADDED
frontend/src/reportWebVitals.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const reportWebVitals = onPerfEntry => {
2
+ if (onPerfEntry && onPerfEntry instanceof Function) {
3
+ import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4
+ getCLS(onPerfEntry);
5
+ getFID(onPerfEntry);
6
+ getFCP(onPerfEntry);
7
+ getLCP(onPerfEntry);
8
+ getTTFB(onPerfEntry);
9
+ });
10
+ }
11
+ };
12
+
13
+ export default reportWebVitals;
frontend/src/setupTests.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // jest-dom adds custom jest matchers for asserting on DOM nodes.
2
+ // allows you to do things like:
3
+ // expect(element).toHaveTextContent(/react/i)
4
+ // learn more: https://github.com/testing-library/jest-dom
5
+ import '@testing-library/jest-dom';