Spaces:
Running
Running
Upload 21 files
Browse files- .env.example +8 -0
- README.md +0 -81
- public/favicon.ico +0 -0
- public/index.html +28 -0
- public/logo192.png +0 -0
- public/logo512.png +0 -0
- public/manifest.json +25 -0
- public/robots.txt +3 -0
- src/App.css +38 -0
- src/App.js +772 -0
- src/App.test.js +8 -0
- src/index.css +9 -0
- src/index.js +10 -0
- src/logo.svg +1 -0
- src/reportWebVitals.js +13 -0
- src/services/apiService.js +101 -0
- src/setupTests.js +5 -0
.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
|
README.md
CHANGED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Medical Summarizer
|
| 3 |
-
emoji: 🐠
|
| 4 |
-
colorFrom: indigo
|
| 5 |
-
colorTo: red
|
| 6 |
-
sdk: static
|
| 7 |
-
pinned: false
|
| 8 |
-
app_build_command: npm run build
|
| 9 |
-
app_file: build/index.html
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
-
# Getting Started with Create React App
|
| 13 |
-
|
| 14 |
-
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
| 15 |
-
|
| 16 |
-
## Available Scripts
|
| 17 |
-
|
| 18 |
-
In the project directory, you can run:
|
| 19 |
-
|
| 20 |
-
### `npm start`
|
| 21 |
-
|
| 22 |
-
Runs the app in the development mode.\
|
| 23 |
-
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
| 24 |
-
|
| 25 |
-
The page will reload when you make changes.\
|
| 26 |
-
You may also see any lint errors in the console.
|
| 27 |
-
|
| 28 |
-
### `npm test`
|
| 29 |
-
|
| 30 |
-
Launches the test runner in the interactive watch mode.\
|
| 31 |
-
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 32 |
-
|
| 33 |
-
### `npm run build`
|
| 34 |
-
|
| 35 |
-
Builds the app for production to the `build` folder.\
|
| 36 |
-
It correctly bundles React in production mode and optimizes the build for the best performance.
|
| 37 |
-
|
| 38 |
-
The build is minified and the filenames include the hashes.\
|
| 39 |
-
Your app is ready to be deployed!
|
| 40 |
-
|
| 41 |
-
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
| 42 |
-
|
| 43 |
-
### `npm run eject`
|
| 44 |
-
|
| 45 |
-
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
| 46 |
-
|
| 47 |
-
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
| 48 |
-
|
| 49 |
-
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
| 50 |
-
|
| 51 |
-
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
| 52 |
-
|
| 53 |
-
## Learn More
|
| 54 |
-
|
| 55 |
-
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
| 56 |
-
|
| 57 |
-
To learn React, check out the [React documentation](https://reactjs.org/).
|
| 58 |
-
|
| 59 |
-
### Code Splitting
|
| 60 |
-
|
| 61 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
| 62 |
-
|
| 63 |
-
### Analyzing the Bundle Size
|
| 64 |
-
|
| 65 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
| 66 |
-
|
| 67 |
-
### Making a Progressive Web App
|
| 68 |
-
|
| 69 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
| 70 |
-
|
| 71 |
-
### Advanced Configuration
|
| 72 |
-
|
| 73 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
| 74 |
-
|
| 75 |
-
### Deployment
|
| 76 |
-
|
| 77 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
| 78 |
-
|
| 79 |
-
### `npm run build` fails to minify
|
| 80 |
-
|
| 81 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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';
|