ntphuc149 commited on
Commit
c3b8c5f
·
verified ·
1 Parent(s): c6db123

Upload 21 files

Browse files
.env.example ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Frontend Environment Variables
2
+ REACT_APP_API_URL=http://localhost:8000
3
+ REACT_APP_ENV=development
4
+
5
+ # Backend Environment Variables
6
+ QWEN_SERVER_IP=https://your-kaggle-server.ngrok.io
7
+ WEB_API_PORT=8000
8
+ WEB_API_HOST=0.0.0.0
.gitattributes ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ public/Aries.png filter=lfs diff=lfs merge=lfs -text
37
+ public/Gemini.jpg filter=lfs diff=lfs merge=lfs -text
38
+ public/Leo.jpg filter=lfs diff=lfs merge=lfs -text
39
+ public/Libra.jpg filter=lfs diff=lfs merge=lfs -text
40
+ public/Pisces.jpg filter=lfs diff=lfs merge=lfs -text
41
+ public/Sagittarius.jpg filter=lfs diff=lfs merge=lfs -text
42
+ public/Taurus.jpg filter=lfs diff=lfs merge=lfs -text
43
+ public/Virgo.jpg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # production
12
+ /build
13
+
14
+ # misc
15
+ .DS_Store
16
+ .env.local
17
+ .env.development.local
18
+ .env.test.local
19
+ .env.production.local
20
+
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+
25
+ # api keys
26
+ .env
27
+ .env.local
28
+ .env.development.local
29
+ .env.test.local
30
+ .env.production.local
README.md ADDED
File without changes
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "medical-mas-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "homepage": ".",
6
+ "dependencies": {
7
+ "lucide-react": "^0.542.0",
8
+ "react": "^18.2.0",
9
+ "react-dom": "^18.2.0",
10
+ "react-scripts": "5.0.1"
11
+ },
12
+ "scripts": {
13
+ "start": "react-scripts start",
14
+ "build": "react-scripts build",
15
+ "test": "react-scripts test",
16
+ "eject": "react-scripts eject"
17
+ },
18
+ "eslintConfig": {
19
+ "extends": [
20
+ "react-app"
21
+ ]
22
+ },
23
+ "browserslist": {
24
+ "production": [
25
+ ">0.2%",
26
+ "not dead",
27
+ "not op_mini all"
28
+ ],
29
+ "development": [
30
+ "last 1 chrome version",
31
+ "last 1 firefox version",
32
+ "last 1 safari version"
33
+ ]
34
+ },
35
+ "proxy": "http://localhost:8000"
36
+ }
public/favicon.ico ADDED
public/index.html ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="theme-color" content="#000000" />
7
+ <meta
8
+ name="description"
9
+ content="Customization Patient Healcare with AI Powered Multi-Agent System"
10
+ />
11
+ <title>Medical Multi-Agent System</title>
12
+ <script src="https://cdn.tailwindcss.com"></script>
13
+
14
+ <link
15
+ href="https://cdn.jsdelivr.net/npm/remixicon@4.1.0/fonts/remixicon.css"
16
+ rel="stylesheet"
17
+ />
18
+
19
+ <link
20
+ rel="stylesheet"
21
+ href="https://cdn.jsdelivr.net/npm/@tabler/icons-sprite@latest/dist/tabler-icons.css"
22
+ />
23
+ </head>
24
+ <body>
25
+ <noscript>You need to enable JavaScript to run this app.</noscript>
26
+ <div id="root"></div>
27
+ </body>
28
+ </html>
public/logo192.png ADDED
public/logo512.png ADDED
public/manifest.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "React App",
3
+ "name": "Create React App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#000000",
24
+ "background_color": "#ffffff"
25
+ }
public/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
src/App.css ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .App {
2
+ text-align: center;
3
+ }
4
+
5
+ .App-logo {
6
+ height: 40vmin;
7
+ pointer-events: none;
8
+ }
9
+
10
+ @media (prefers-reduced-motion: no-preference) {
11
+ .App-logo {
12
+ animation: App-logo-spin infinite 20s linear;
13
+ }
14
+ }
15
+
16
+ .App-header {
17
+ background-color: #282c34;
18
+ min-height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ font-size: calc(10px + 2vmin);
24
+ color: white;
25
+ }
26
+
27
+ .App-link {
28
+ color: #61dafb;
29
+ }
30
+
31
+ @keyframes App-logo-spin {
32
+ from {
33
+ transform: rotate(0deg);
34
+ }
35
+ to {
36
+ transform: rotate(360deg);
37
+ }
38
+ }
src/App.js ADDED
@@ -0,0 +1,772 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import {
3
+ Send,
4
+ Moon,
5
+ Sun,
6
+ User,
7
+ Bot,
8
+ AlertCircle,
9
+ CheckCircle,
10
+ Clock,
11
+ Heart,
12
+ Phone,
13
+ Calendar,
14
+ MapPin,
15
+ FileText,
16
+ } from "lucide-react";
17
+ import { apiService } from "./services/apiService";
18
+
19
+ const MedicalChatUI = () => {
20
+ // UI State
21
+ const [isDarkMode, setIsDarkMode] = useState(false);
22
+ const [currentMessage, setCurrentMessage] = useState("");
23
+ const [messages, setMessages] = useState([]);
24
+ const [isTyping, setIsTyping] = useState(false);
25
+ const [isConnected, setIsConnected] = useState(false);
26
+ const [connectionError, setConnectionError] = useState("");
27
+
28
+ // Patient Data State
29
+ const [patientRecord, setPatientRecord] = useState({
30
+ personal_info: {
31
+ full_name: "",
32
+ dob: "",
33
+ gender: "",
34
+ phone: "",
35
+ address: "",
36
+ },
37
+ medical_info: {
38
+ current_symptoms: [],
39
+ symptom_start_date: "",
40
+ previous_medications: [],
41
+ allergies: [],
42
+ severity_level: "",
43
+ },
44
+ });
45
+
46
+ // Session State
47
+ const [sessionStatus, setSessionStatus] = useState("idle"); // idle | collecting | completed
48
+ const [confidenceScores, setConfidenceScores] = useState({
49
+ extraction: 0,
50
+ validation: 0,
51
+ });
52
+
53
+ const messagesEndRef = useRef(null);
54
+
55
+ // Auto-scroll to bottom
56
+ const scrollToBottom = () => {
57
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
58
+ };
59
+
60
+ useEffect(() => {
61
+ scrollToBottom();
62
+ }, [messages]);
63
+
64
+ // Initialize connection and start chat
65
+ useEffect(() => {
66
+ initializeChat();
67
+ }, []);
68
+
69
+ const initializeChat = async () => {
70
+ try {
71
+ // Health check
72
+ await apiService.healthCheck();
73
+ setIsConnected(true);
74
+ setConnectionError("");
75
+
76
+ // Start chat session
77
+ const response = await apiService.startChat();
78
+
79
+ // Add initial AI message
80
+ setMessages([
81
+ {
82
+ id: Date.now(),
83
+ text: response.message,
84
+ sender: "ai",
85
+ timestamp: new Date().toISOString(),
86
+ },
87
+ ]);
88
+
89
+ setSessionStatus("collecting");
90
+ } catch (error) {
91
+ setIsConnected(false);
92
+ setConnectionError(error.message);
93
+ console.error("Failed to initialize chat:", error);
94
+ }
95
+ };
96
+
97
+ const handleSendMessage = async () => {
98
+ if (!currentMessage.trim() || isTyping) return;
99
+
100
+ const userMessage = {
101
+ id: Date.now(),
102
+ text: currentMessage.trim(),
103
+ sender: "user",
104
+ timestamp: new Date().toISOString(),
105
+ };
106
+
107
+ // Add user message to chat
108
+ setMessages((prev) => [...prev, userMessage]);
109
+ setCurrentMessage("");
110
+ setIsTyping(true);
111
+
112
+ try {
113
+ // Send to backend
114
+ const response = await apiService.sendMessage(userMessage.text);
115
+
116
+ // Update patient record
117
+ setPatientRecord(response.patient_record);
118
+ setConfidenceScores(response.confidence_scores);
119
+ setSessionStatus(response.session_status);
120
+
121
+ // Add AI response
122
+ const aiMessage = {
123
+ id: Date.now() + 1,
124
+ text: response.ai_message,
125
+ sender: "ai",
126
+ timestamp: new Date().toISOString(),
127
+ metadata: response.metadata,
128
+ };
129
+
130
+ setMessages((prev) => [...prev, aiMessage]);
131
+
132
+ // If conversation completed, show completion message
133
+ if (response.should_end) {
134
+ setTimeout(() => {
135
+ const completionMessage = {
136
+ id: Date.now() + 2,
137
+ text: "✅ Information collection completed! Click 'Generate Summary' to create a doctor report.",
138
+ sender: "system",
139
+ timestamp: new Date().toISOString(),
140
+ };
141
+ setMessages((prev) => [...prev, completionMessage]);
142
+ }, 1000);
143
+ }
144
+ } catch (error) {
145
+ console.error("Failed to send message:", error);
146
+
147
+ // Add error message
148
+ const errorMessage = {
149
+ id: Date.now() + 1,
150
+ text: `❌ Error: ${error.message}. Please try again.`,
151
+ sender: "system",
152
+ timestamp: new Date().toISOString(),
153
+ };
154
+ setMessages((prev) => [...prev, errorMessage]);
155
+ } finally {
156
+ setIsTyping(false);
157
+ }
158
+ };
159
+
160
+ const handleGenerateSummary = async () => {
161
+ try {
162
+ setIsTyping(true);
163
+ const summary = await apiService.generateSummary();
164
+
165
+ const summaryMessage = {
166
+ id: Date.now(),
167
+ text: `📋 **Doctor Summary Generated**\n\n**Summary:** ${
168
+ summary.doctor_summary
169
+ }\n\n**Urgency Level:** ${
170
+ summary.urgency_level
171
+ }\n\n**Key Findings:**\n${summary.key_findings
172
+ .map((f) => `• ${f}`)
173
+ .join(
174
+ "\n"
175
+ )}\n\n**Recommended Questions:**\n${summary.recommended_questions
176
+ .map((q) => `• ${q}`)
177
+ .join("\n")}`,
178
+ sender: "system",
179
+ timestamp: new Date().toISOString(),
180
+ };
181
+
182
+ setMessages((prev) => [...prev, summaryMessage]);
183
+ } catch (error) {
184
+ console.error("Failed to generate summary:", error);
185
+ } finally {
186
+ setIsTyping(false);
187
+ }
188
+ };
189
+
190
+ const handleResetChat = async () => {
191
+ try {
192
+ await apiService.resetSession();
193
+ setMessages([]);
194
+ setPatientRecord({
195
+ personal_info: {
196
+ full_name: "",
197
+ dob: "",
198
+ gender: "",
199
+ phone: "",
200
+ address: "",
201
+ },
202
+ medical_info: {
203
+ current_symptoms: [],
204
+ symptom_start_date: "",
205
+ previous_medications: [],
206
+ allergies: [],
207
+ severity_level: "",
208
+ },
209
+ });
210
+ setSessionStatus("idle");
211
+ setConfidenceScores({ extraction: 0, validation: 0 });
212
+
213
+ // Restart chat
214
+ await initializeChat();
215
+ } catch (error) {
216
+ console.error("Failed to reset chat:", error);
217
+ }
218
+ };
219
+
220
+ // Components
221
+ const MessageBubble = ({ message }) => {
222
+ const isUser = message.sender === "user";
223
+ const isSystem = message.sender === "system";
224
+
225
+ return (
226
+ <div className={`flex ${isUser ? "justify-end" : "justify-start"} mb-4`}>
227
+ <div className="flex items-start space-x-3 max-w-3xl">
228
+ {!isUser && (
229
+ <div
230
+ className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
231
+ isSystem
232
+ ? isDarkMode
233
+ ? "bg-yellow-900/50 text-yellow-300"
234
+ : "bg-yellow-100 text-yellow-700"
235
+ : isDarkMode
236
+ ? "bg-blue-600"
237
+ : "bg-blue-500"
238
+ }`}
239
+ >
240
+ {isSystem ? (
241
+ <AlertCircle className="w-4 h-4" />
242
+ ) : (
243
+ <Bot className="w-4 h-4 text-white" />
244
+ )}
245
+ </div>
246
+ )}
247
+
248
+ <div
249
+ className={`px-4 py-3 rounded-2xl ${
250
+ isUser
251
+ ? isDarkMode
252
+ ? "bg-blue-600 text-white"
253
+ : "bg-blue-500 text-white"
254
+ : isSystem
255
+ ? isDarkMode
256
+ ? "bg-yellow-900/20 text-yellow-200 border border-yellow-700"
257
+ : "bg-yellow-50 text-yellow-800 border border-yellow-200"
258
+ : isDarkMode
259
+ ? "bg-gray-700 text-gray-200"
260
+ : "bg-gray-100 text-gray-900"
261
+ }`}
262
+ >
263
+ <div className="whitespace-pre-wrap">{message.text}</div>
264
+
265
+ {message.metadata && (
266
+ <div className="text-xs mt-2 opacity-70">
267
+ Exchange #{message.metadata.exchange_count} | Confidence:{" "}
268
+ {(message.metadata.confidence || 0).toFixed(2)}
269
+ </div>
270
+ )}
271
+ </div>
272
+
273
+ {isUser && (
274
+ <div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-400 flex items-center justify-center">
275
+ <User className="w-4 h-4 text-white" />
276
+ </div>
277
+ )}
278
+ </div>
279
+ </div>
280
+ );
281
+ };
282
+
283
+ const TypingIndicator = () => (
284
+ <div className="flex justify-start mb-4">
285
+ <div className="flex items-center space-x-3 max-w-xs">
286
+ <div
287
+ className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
288
+ isDarkMode ? "bg-blue-600" : "bg-blue-500"
289
+ }`}
290
+ >
291
+ <Bot className="w-4 h-4 text-white" />
292
+ </div>
293
+ <div
294
+ className={`px-4 py-3 rounded-2xl ${
295
+ isDarkMode ? "bg-gray-700" : "bg-gray-100"
296
+ }`}
297
+ >
298
+ <div className="flex space-x-1">
299
+ <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
300
+ <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce [animation-delay:0.1s]"></div>
301
+ <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce [animation-delay:0.2s]"></div>
302
+ </div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ );
307
+
308
+ const PatientPanel = () => (
309
+ <div
310
+ className={`w-96 ${
311
+ isDarkMode ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"
312
+ } border-l flex flex-col`}
313
+ >
314
+ {/* Header */}
315
+ <div
316
+ className={`p-4 border-b ${
317
+ isDarkMode ? "border-gray-700" : "border-gray-200"
318
+ }`}
319
+ >
320
+ <h2
321
+ className={`text-lg font-semibold ${
322
+ isDarkMode ? "text-gray-200" : "text-gray-900"
323
+ } flex items-center`}
324
+ >
325
+ <FileText className="w-5 h-5 mr-2" />
326
+ Patient Record
327
+ </h2>
328
+ <div className="flex items-center mt-2 space-x-4">
329
+ <div
330
+ className={`text-sm ${
331
+ isDarkMode ? "text-gray-400" : "text-gray-600"
332
+ }`}
333
+ >
334
+ Status: {sessionStatus}
335
+ </div>
336
+ <div className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
337
+ Confidence:{" "}
338
+ {Math.round(
339
+ ((confidenceScores.extraction + confidenceScores.validation) /
340
+ 2) *
341
+ 100
342
+ )}
343
+ %
344
+ </div>
345
+ </div>
346
+ </div>
347
+
348
+ {/* Content */}
349
+ <div className="flex-1 overflow-y-auto p-4 space-y-6">
350
+ {/* Personal Information */}
351
+ <div>
352
+ <h3
353
+ className={`text-sm font-medium ${
354
+ isDarkMode ? "text-gray-300" : "text-gray-700"
355
+ } mb-3 flex items-center`}
356
+ >
357
+ <User className="w-4 h-4 mr-2" />
358
+ Personal Information
359
+ </h3>
360
+
361
+ <div className="space-y-3">
362
+ <div>
363
+ <span
364
+ className={`text-sm ${
365
+ isDarkMode ? "text-gray-400" : "text-gray-600"
366
+ }`}
367
+ >
368
+ Name:
369
+ </span>
370
+ <p
371
+ className={`text-sm mt-1 ${
372
+ isDarkMode ? "text-gray-200" : "text-gray-900"
373
+ }`}
374
+ >
375
+ {patientRecord.personal_info.full_name || "Not provided"}
376
+ </p>
377
+ </div>
378
+
379
+ <div>
380
+ <span
381
+ className={`text-sm ${
382
+ isDarkMode ? "text-gray-400" : "text-gray-600"
383
+ }`}
384
+ >
385
+ Date of Birth:
386
+ </span>
387
+ <p
388
+ className={`text-sm mt-1 ${
389
+ isDarkMode ? "text-gray-200" : "text-gray-900"
390
+ } flex items-center`}
391
+ >
392
+ <Calendar className="w-3 h-3 mr-1" />
393
+ {patientRecord.personal_info.dob || "Not provided"}
394
+ </p>
395
+ </div>
396
+
397
+ <div>
398
+ <span
399
+ className={`text-sm ${
400
+ isDarkMode ? "text-gray-400" : "text-gray-600"
401
+ }`}
402
+ >
403
+ Phone:
404
+ </span>
405
+ <p
406
+ className={`text-sm mt-1 ${
407
+ isDarkMode ? "text-gray-200" : "text-gray-900"
408
+ } flex items-center`}
409
+ >
410
+ <Phone className="w-3 h-3 mr-1" />
411
+ {patientRecord.personal_info.phone || "Not provided"}
412
+ </p>
413
+ </div>
414
+
415
+ <div>
416
+ <span
417
+ className={`text-sm ${
418
+ isDarkMode ? "text-gray-400" : "text-gray-600"
419
+ }`}
420
+ >
421
+ Address:
422
+ </span>
423
+ <p
424
+ className={`text-sm mt-1 ${
425
+ isDarkMode ? "text-gray-200" : "text-gray-900"
426
+ } flex items-center`}
427
+ >
428
+ <MapPin className="w-3 h-3 mr-1" />
429
+ {patientRecord.personal_info.address || "Not provided"}
430
+ </p>
431
+ </div>
432
+ </div>
433
+ </div>
434
+
435
+ {/* Medical Information */}
436
+ <div>
437
+ <h3
438
+ className={`text-sm font-medium ${
439
+ isDarkMode ? "text-gray-300" : "text-gray-700"
440
+ } mb-3 flex items-center`}
441
+ >
442
+ <Heart className="w-4 h-4 mr-2" />
443
+ Medical Information
444
+ </h3>
445
+
446
+ <div className="space-y-3">
447
+ <div>
448
+ <span
449
+ className={`text-sm ${
450
+ isDarkMode ? "text-gray-400" : "text-gray-600"
451
+ }`}
452
+ >
453
+ Symptoms:
454
+ </span>
455
+ <p
456
+ className={`text-sm mt-1 ${
457
+ isDarkMode ? "text-gray-200" : "text-gray-900"
458
+ }`}
459
+ >
460
+ {patientRecord.medical_info.current_symptoms?.length > 0
461
+ ? patientRecord.medical_info.current_symptoms.join(", ")
462
+ : "Not provided"}
463
+ </p>
464
+ </div>
465
+
466
+ <div>
467
+ <span
468
+ className={`text-sm ${
469
+ isDarkMode ? "text-gray-400" : "text-gray-600"
470
+ }`}
471
+ >
472
+ Duration:
473
+ </span>
474
+ <p
475
+ className={`text-sm mt-1 ${
476
+ isDarkMode ? "text-gray-200" : "text-gray-900"
477
+ } flex items-center`}
478
+ >
479
+ <Clock className="w-3 h-3 mr-1" />
480
+ {patientRecord.medical_info.symptom_start_date ||
481
+ "Not provided"}
482
+ </p>
483
+ </div>
484
+
485
+ <div>
486
+ <span
487
+ className={`text-sm ${
488
+ isDarkMode ? "text-gray-400" : "text-gray-600"
489
+ }`}
490
+ >
491
+ Severity:
492
+ </span>
493
+ <p
494
+ className={`text-sm mt-1 ${
495
+ isDarkMode ? "text-gray-200" : "text-gray-900"
496
+ }`}
497
+ >
498
+ {patientRecord.medical_info.severity_level || "Not assessed"}
499
+ </p>
500
+ </div>
501
+
502
+ <div>
503
+ <span
504
+ className={`text-sm ${
505
+ isDarkMode ? "text-gray-400" : "text-gray-600"
506
+ }`}
507
+ >
508
+ Medications:
509
+ </span>
510
+ <p
511
+ className={`text-sm mt-1 ${
512
+ isDarkMode ? "text-gray-200" : "text-gray-900"
513
+ }`}
514
+ >
515
+ {patientRecord.medical_info.previous_medications?.length > 0
516
+ ? patientRecord.medical_info.previous_medications.join(", ")
517
+ : "None reported"}
518
+ </p>
519
+ </div>
520
+
521
+ <div>
522
+ <span
523
+ className={`text-sm ${
524
+ isDarkMode ? "text-gray-400" : "text-gray-600"
525
+ }`}
526
+ >
527
+ Allergies:
528
+ </span>
529
+ <p
530
+ className={`text-sm mt-1 ${
531
+ isDarkMode ? "text-gray-200" : "text-gray-900"
532
+ }`}
533
+ >
534
+ {patientRecord.medical_info.allergies?.length > 0
535
+ ? patientRecord.medical_info.allergies.join(", ")
536
+ : "None reported"}
537
+ </p>
538
+ </div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ {/* Actions */}
544
+ <div
545
+ className={`p-4 border-t ${
546
+ isDarkMode ? "border-gray-700" : "border-gray-200"
547
+ } space-y-2`}
548
+ >
549
+ <button
550
+ onClick={handleGenerateSummary}
551
+ disabled={sessionStatus === "idle"}
552
+ className={`w-full px-4 py-2 rounded-lg transition-colors ${
553
+ sessionStatus === "idle"
554
+ ? `${
555
+ isDarkMode
556
+ ? "bg-gray-700 text-gray-500 cursor-not-allowed"
557
+ : "bg-gray-200 text-gray-400 cursor-not-allowed"
558
+ }`
559
+ : `${
560
+ isDarkMode
561
+ ? "bg-blue-600 hover:bg-blue-700"
562
+ : "bg-blue-500 hover:bg-blue-600"
563
+ } text-white`
564
+ }`}
565
+ >
566
+ Generate Summary
567
+ </button>
568
+ <button
569
+ onClick={handleResetChat}
570
+ className={`w-full px-4 py-2 rounded-lg transition-colors ${
571
+ isDarkMode
572
+ ? "border border-gray-600 hover:bg-gray-700 text-gray-300"
573
+ : "border border-gray-300 hover:bg-gray-50 text-gray-700"
574
+ }`}
575
+ >
576
+ New Session
577
+ </button>
578
+ </div>
579
+ </div>
580
+ );
581
+
582
+ // Connection error screen
583
+ if (!isConnected && connectionError) {
584
+ return (
585
+ <div
586
+ className={`h-screen flex items-center justify-center ${
587
+ isDarkMode ? "bg-gray-900" : "bg-gray-50"
588
+ }`}
589
+ >
590
+ <div
591
+ className={`text-center p-8 rounded-lg ${
592
+ isDarkMode ? "bg-gray-800" : "bg-white"
593
+ } shadow-lg`}
594
+ >
595
+ <AlertCircle
596
+ className={`w-16 h-16 mx-auto mb-4 ${
597
+ isDarkMode ? "text-red-400" : "text-red-500"
598
+ }`}
599
+ />
600
+ <h2
601
+ className={`text-xl font-semibold mb-2 ${
602
+ isDarkMode ? "text-gray-200" : "text-gray-900"
603
+ }`}
604
+ >
605
+ Connection Failed
606
+ </h2>
607
+ <p
608
+ className={`mb-4 ${isDarkMode ? "text-gray-400" : "text-gray-600"}`}
609
+ >
610
+ {connectionError}
611
+ </p>
612
+ <button
613
+ onClick={initializeChat}
614
+ className={`px-4 py-2 rounded-lg ${
615
+ isDarkMode
616
+ ? "bg-blue-600 hover:bg-blue-700"
617
+ : "bg-blue-500 hover:bg-blue-600"
618
+ } text-white transition-colors`}
619
+ >
620
+ Retry Connection
621
+ </button>
622
+ </div>
623
+ </div>
624
+ );
625
+ }
626
+
627
+ return (
628
+ <div
629
+ className={`h-screen flex ${isDarkMode ? "bg-gray-900" : "bg-gray-50"}`}
630
+ >
631
+ {/* Main Content */}
632
+ <div className="flex-1 flex flex-col">
633
+ {/* Header */}
634
+ <header
635
+ className={`${
636
+ isDarkMode
637
+ ? "bg-gray-800 border-gray-700"
638
+ : "bg-white border-gray-200"
639
+ } border-b px-6 py-4 flex items-center justify-between`}
640
+ >
641
+ <div className="flex items-center space-x-4">
642
+ <h1
643
+ className={`text-xl font-semibold ${
644
+ isDarkMode ? "text-gray-200" : "text-gray-900"
645
+ }`}
646
+ >
647
+ Medical Multi-Agent System
648
+ </h1>
649
+ </div>
650
+
651
+ <div className="flex items-center space-x-2">
652
+ <div
653
+ className={`px-3 py-1 rounded-full text-sm flex items-center space-x-2 ${
654
+ isConnected
655
+ ? isDarkMode
656
+ ? "bg-green-900/50 text-green-300 border border-green-700"
657
+ : "bg-green-100 text-green-700 border border-green-200"
658
+ : isDarkMode
659
+ ? "bg-red-900/50 text-red-300 border border-red-700"
660
+ : "bg-red-100 text-red-700 border border-red-200"
661
+ }`}
662
+ >
663
+ {isConnected ? (
664
+ <CheckCircle className="w-3 h-3" />
665
+ ) : (
666
+ <AlertCircle className="w-3 h-3" />
667
+ )}
668
+ <span>
669
+ {isConnected ? "Connected to Qwen 3-4B" : "Disconnected"}
670
+ </span>
671
+ </div>
672
+ <button
673
+ onClick={() => setIsDarkMode(!isDarkMode)}
674
+ className={`p-2 rounded-lg ${
675
+ isDarkMode
676
+ ? "hover:bg-gray-700 text-gray-300"
677
+ : "hover:bg-gray-100 text-gray-600"
678
+ } transition-colors`}
679
+ >
680
+ {isDarkMode ? (
681
+ <Sun className="w-5 h-5" />
682
+ ) : (
683
+ <Moon className="w-5 h-5" />
684
+ )}
685
+ </button>
686
+ </div>
687
+ </header>
688
+
689
+ <div className="flex-1 flex min-h-0 overflow-hidden">
690
+ {/* Chat Area */}
691
+ <div className="flex-1 flex flex-col">
692
+ {/* Messages */}
693
+ <div className="flex-1 overflow-y-auto p-6">
694
+ {messages.length === 0 && (
695
+ <div className="flex items-center justify-center h-full">
696
+ <div className="text-center">
697
+ <Bot
698
+ className={`w-16 h-16 mx-auto mb-4 ${
699
+ isDarkMode ? "text-gray-600" : "text-gray-400"
700
+ }`}
701
+ />
702
+ <p
703
+ className={`text-lg ${
704
+ isDarkMode ? "text-gray-400" : "text-gray-600"
705
+ }`}
706
+ >
707
+ Initializing Medical Multi-Agent System...
708
+ </p>
709
+ </div>
710
+ </div>
711
+ )}
712
+
713
+ {messages.map((message) => (
714
+ <MessageBubble key={message.id} message={message} />
715
+ ))}
716
+ {isTyping && <TypingIndicator />}
717
+ <div ref={messagesEndRef} />
718
+ </div>
719
+
720
+ {/* Input Area */}
721
+ <div
722
+ className={`p-6 border-t ${
723
+ isDarkMode ? "border-gray-700" : "border-gray-200"
724
+ }`}
725
+ >
726
+ <div className="flex space-x-4">
727
+ <input
728
+ type="text"
729
+ value={currentMessage}
730
+ onChange={(e) => setCurrentMessage(e.target.value)}
731
+ onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
732
+ placeholder="Describe your symptoms or provide requested information..."
733
+ disabled={!isConnected}
734
+ className={`flex-1 px-4 py-3 rounded-lg border ${
735
+ isDarkMode
736
+ ? "bg-gray-700 border-gray-600 text-gray-200 placeholder-gray-400"
737
+ : "bg-white border-gray-300 text-gray-900 placeholder-gray-500"
738
+ } focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50`}
739
+ />
740
+ <button
741
+ onClick={handleSendMessage}
742
+ disabled={!currentMessage.trim() || isTyping || !isConnected}
743
+ className={`px-6 py-3 rounded-lg transition-colors flex items-center space-x-2 ${
744
+ !currentMessage.trim() || isTyping || !isConnected
745
+ ? `${
746
+ isDarkMode
747
+ ? "bg-gray-700 text-gray-500"
748
+ : "bg-gray-200 text-gray-400"
749
+ } cursor-not-allowed`
750
+ : `${
751
+ isDarkMode
752
+ ? "bg-blue-600 hover:bg-blue-700"
753
+ : "bg-blue-500 hover:bg-blue-600"
754
+ } text-white`
755
+ }`}
756
+ >
757
+ <Send className="w-4 h-4" />
758
+ <span>{isTyping ? "Sending..." : "Send"}</span>
759
+ </button>
760
+ </div>
761
+ </div>
762
+ </div>
763
+
764
+ {/* Patient Panel */}
765
+ <PatientPanel />
766
+ </div>
767
+ </div>
768
+ </div>
769
+ );
770
+ };
771
+
772
+ export default MedicalChatUI;
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
+ });
src/index.css ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom styles nếu cần */
6
+ body {
7
+ margin: 0;
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
9
+ }
src/index.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const root = ReactDOM.createRoot(document.getElementById('root'));
6
+ root.render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
src/logo.svg ADDED
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;
src/services/apiService.js ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API Service for Medical Multi-Agent System
3
+ * Handles communication with backend Web API
4
+ */
5
+
6
+ const API_BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000";
7
+
8
+ class ApiService {
9
+ constructor() {
10
+ this.baseUrl = API_BASE_URL;
11
+ }
12
+
13
+ /**
14
+ * Generic API call wrapper
15
+ */
16
+ async apiCall(endpoint, options = {}) {
17
+ const url = `${this.baseUrl}${endpoint}`;
18
+ const config = {
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ ...options.headers,
22
+ },
23
+ ...options,
24
+ };
25
+
26
+ try {
27
+ const response = await fetch(url, config);
28
+
29
+ if (!response.ok) {
30
+ const errorData = await response
31
+ .json()
32
+ .catch(() => ({ detail: "Unknown error" }));
33
+ throw new Error(
34
+ `API Error: ${response.status} - ${
35
+ errorData.detail || response.statusText
36
+ }`
37
+ );
38
+ }
39
+
40
+ return await response.json();
41
+ } catch (error) {
42
+ console.error("API call failed:", error);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Health check
49
+ */
50
+ async healthCheck() {
51
+ return this.apiCall("/api/health");
52
+ }
53
+
54
+ /**
55
+ * Start new chat session
56
+ */
57
+ async startChat() {
58
+ return this.apiCall("/api/chat/start", {
59
+ method: "POST",
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Send message to AI
65
+ */
66
+ async sendMessage(message) {
67
+ return this.apiCall("/api/chat/send", {
68
+ method: "POST",
69
+ body: JSON.stringify({ message }),
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Get current session status and patient record
75
+ */
76
+ async getSessionStatus() {
77
+ return this.apiCall("/api/chat/status");
78
+ }
79
+
80
+ /**
81
+ * Generate doctor summary
82
+ */
83
+ async generateSummary() {
84
+ return this.apiCall("/api/chat/summary", {
85
+ method: "POST",
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Reset current session
91
+ */
92
+ async resetSession() {
93
+ return this.apiCall("/api/chat/reset", {
94
+ method: "DELETE",
95
+ });
96
+ }
97
+ }
98
+
99
+ // Export singleton instance
100
+ export const apiService = new ApiService();
101
+ export default apiService;
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';