AnkTechsol commited on
Commit
fb95f15
·
0 Parent(s):

Initial commit with Git LFS

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +12 -0
  2. .gitattributes +4 -0
  3. .gitignore +10 -0
  4. Dockerfile +84 -0
  5. LICENSE +201 -0
  6. README.md +79 -0
  7. app.py +94 -0
  8. appoint-ready +1 -0
  9. cache_manager.py +46 -0
  10. config.py +97 -0
  11. frontend/package-lock.json +0 -0
  12. frontend/package.json +31 -0
  13. frontend/public/assets/ai_headshot.svg +1 -0
  14. frontend/public/assets/alex.avif +3 -0
  15. frontend/public/assets/alex.mp4 +3 -0
  16. frontend/public/assets/alex_300.avif +3 -0
  17. frontend/public/assets/alex_fhir.json +246 -0
  18. frontend/public/assets/gemini.avif +3 -0
  19. frontend/public/assets/jason_fhir.json +167 -0
  20. frontend/public/assets/jordan.avif +3 -0
  21. frontend/public/assets/jordan.mp4 +3 -0
  22. frontend/public/assets/jordan_300.avif +3 -0
  23. frontend/public/assets/medgemma.avif +3 -0
  24. frontend/public/assets/patients_and_conditions.json +46 -0
  25. frontend/public/assets/sacha.avif +3 -0
  26. frontend/public/assets/sacha.mp4 +3 -0
  27. frontend/public/assets/sacha_150.avif +3 -0
  28. frontend/public/assets/sacha_fhir.json +266 -0
  29. frontend/public/assets/welcome_bottom_graphics.svg +1 -0
  30. frontend/public/assets/welcome_graphics.svg +1 -0
  31. frontend/public/assets/welcome_top_graphics.svg +1 -0
  32. frontend/public/index.html +30 -0
  33. frontend/src/App.js +103 -0
  34. frontend/src/components/DetailsPopup/DetailsPopup.css +46 -0
  35. frontend/src/components/DetailsPopup/DetailsPopup.js +76 -0
  36. frontend/src/components/Interview/Interview.css +325 -0
  37. frontend/src/components/Interview/Interview.js +499 -0
  38. frontend/src/components/Landing/Landing.css +296 -0
  39. frontend/src/components/Landing/Landing.js +91 -0
  40. frontend/src/components/PatientBuilder/PatientBuilder.css +205 -0
  41. frontend/src/components/PatientBuilder/PatientBuilder.js +243 -0
  42. frontend/src/components/PreloadImages.js +42 -0
  43. frontend/src/components/RadiologyExplainer/RadiologyExplainer.css +424 -0
  44. frontend/src/components/RadiologyExplainer/RadiologyExplainer.js +286 -0
  45. frontend/src/components/RolePlayDialogs/RolePlayDialogs.css +101 -0
  46. frontend/src/components/RolePlayDialogs/RolePlayDialogs.js +103 -0
  47. frontend/src/components/WelcomePage/WelcomePage.css +144 -0
  48. frontend/src/components/WelcomePage/WelcomePage.js +72 -0
  49. frontend/src/index.js +23 -0
  50. frontend/src/shared/Style.css +217 -0
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ node_modules
3
+ __pycache__
4
+ *.pyc
5
+ .env
6
+ appoint-ready/
7
+ rad_explain/
8
+ frontend/node_modules/
9
+ frontend/build/
10
+ cache/
11
+ *.zip
12
+ .DS_Store
.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.avif filter=lfs diff=lfs merge=lfs -text
4
+ *.zip filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ env.list
5
+ node_modules/
6
+ frontend/build/
7
+ *.zip
8
+ .DS_Store
9
+ cache/
10
+ /tmp/sushruta*
Dockerfile ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # ==========================================
16
+ # Stage 1: Build the React Frontend
17
+ # ==========================================
18
+ FROM node:24-slim AS frontend-build
19
+ WORKDIR /app/frontend
20
+
21
+ # Copy frontend codebase
22
+ COPY frontend/package*.json ./
23
+ RUN npm install
24
+
25
+ COPY frontend/ ./
26
+ RUN npm run build
27
+
28
+ # ==========================================
29
+ # Stage 2: Build the Unified Python Backend
30
+ # ==========================================
31
+ FROM python:3.12-slim
32
+
33
+ # Install system dependencies
34
+ RUN apt-get update && apt-get install -y \
35
+ wget \
36
+ tar \
37
+ ffmpeg \
38
+ unzip \
39
+ && rm -rf /var/lib/apt/lists/*
40
+
41
+ # Set environment variables
42
+ ENV PYTHONUNBUFFERED=1 \
43
+ PORT=7860 \
44
+ FRONTEND_BUILD=/app/frontend/build \
45
+ CACHE_DIR=/cache
46
+
47
+ # Create user with UID 1000 for Hugging Face Spaces security compliance
48
+ RUN useradd -m -u 1000 user
49
+ WORKDIR /app
50
+
51
+ # Copy requirements and install python packages
52
+ COPY requirements.txt ./
53
+ RUN pip install --no-cache-dir -r requirements.txt
54
+
55
+ # Copy all python source modules and package files
56
+ COPY config.py cache_manager.py llm_client.py tts_client.py app.py LICENSE ./
57
+ COPY intake/ ./intake/
58
+ COPY radiology/ ./radiology/
59
+ COPY static/ ./static/
60
+
61
+ # Copy built React frontend assets from Stage 1
62
+ COPY --from=frontend-build /app/frontend/build ./frontend/build
63
+
64
+ # Set up caching directory with unpacked archives
65
+ RUN mkdir -p /cache/intake /cache/radiology
66
+
67
+ # Copy cache archives
68
+ COPY cache_archives/intake_cache.zip /tmp/intake_cache.zip
69
+ COPY cache_archives/radiology_default_cache/ /tmp/radiology_default_cache/
70
+
71
+ # Extract intake cache
72
+ RUN unzip -q -o /tmp/intake_cache.zip -d /cache/intake && rm /tmp/intake_cache.zip
73
+
74
+ # Copy radiology cache
75
+ RUN cp -r /tmp/radiology_default_cache/* /cache/radiology/ && rm -rf /tmp/radiology_default_cache
76
+
77
+ # Set permissions for user 1000
78
+ RUN chown -R 1000:1000 /app /cache && chmod -R 777 /cache
79
+
80
+ # Switch to the non-root user
81
+ USER user
82
+
83
+ EXPOSE 7860
84
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app", "--threads", "4", "--timeout", "600"]
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Sushruta - Patient 360
3
+ emoji: 🏥
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: true
9
+ license: apache-2.0
10
+ short_description: 'Patient 360 Demo — Pre-Visit Intake & Radiology Explainer'
11
+ models:
12
+ - google/medgemma-27b-text-it
13
+ - google/medgemma-4b-it
14
+ secrets:
15
+ - HF_TOKEN
16
+ - MEDGEMMA_27B_ENDPOINT
17
+ - MEDGEMMA_4B_ENDPOINT
18
+ ---
19
+
20
+ # Sushruta — Patient 360 🏥
21
+
22
+ Sushruta is a unified Patient 360 application that merges two prominent Google Health AI research demonstrations into a single, high-fidelity experience:
23
+ 1. **Pre-Visit Intake Simulator**: Watch MedGemma conduct a pre-visit clinical interview and draft a primary care intake report.
24
+ 2. **Radiology Report Explainer**: Explore radiology reports and images, and click any medical sentence to receive a simplified, clinical-grade plain-language explanation from MedGemma.
25
+
26
+ Both features have been optimized to run completely on **Hugging Face Inference APIs and Endpoints**, utilizing the MedGemma family of models:
27
+ - **Clinical Assistant (Intake)**: `google/medgemma-27b-text-it`
28
+ - **Patient Roleplay**: `google/gemma-3-27b-it`
29
+ - **Radiology Explainer**: `google/medgemma-4b-it`
30
+ - **Text-to-Speech (TTS)**: `hexgrad/Kokoro-82M`
31
+
32
+ ---
33
+
34
+ ## 🛠️ Stack & Architecture
35
+
36
+ - **Backend**: Python, Flask, and Flask-CORS serving high-performance API endpoints and SSE streams.
37
+ - **Frontend**: A gorgeous React 18 SPA styled with a modern, glassmorphic dark medical theme, responsive navigation, and custom visualizations.
38
+ - **Caching**: Shared diskcache memoization allowing seamless demonstrations, instant responses, and zero API costs for pre-simulated scenarios.
39
+ - **Speech**: High-fidelity real-time voice synthesis powered by serverless TTS.
40
+
41
+ ---
42
+
43
+ ## 🚀 Running Locally
44
+
45
+ ### With Docker (Recommended)
46
+ You can run the entire space locally using Docker:
47
+ ```bash
48
+ ./run_local.sh
49
+ ```
50
+ The application will be accessible at http://localhost:7860.
51
+
52
+ ### Without Docker
53
+ 1. **Backend**:
54
+ ```bash
55
+ pip install -r requirements.txt
56
+ export HF_TOKEN="your_huggingface_token"
57
+ python app.py
58
+ ```
59
+ 2. **Frontend**:
60
+ ```bash
61
+ cd frontend
62
+ npm install
63
+ npm run start
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 🔒 Configuration & Secrets
69
+
70
+ When deploying to Hugging Face Spaces, ensure the following secrets are set:
71
+ - `HF_TOKEN`: Hugging Face API access token (required for serverless router and speech endpoints).
72
+ - `MEDGEMMA_27B_ENDPOINT` (Optional): Custom inference endpoint URL for MedGemma 27B. If left blank, falls back to the serverless API.
73
+ - `MEDGEMMA_4B_ENDPOINT` (Optional): Custom inference endpoint URL for MedGemma 4B. If left blank, falls back to the serverless API.
74
+ - `GENERATE_SPEECH`: Set to `true` to enable real-time TTS (default: `false` to use cached recordings).
75
+
76
+ ---
77
+
78
+ ## 📜 License
79
+ Licensed under the Apache License, Version 2.0.
app.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Unified Flask application for Sushruta Patient 360."""
15
+
16
+ import logging
17
+ import os
18
+ from flask import Flask, send_from_directory, jsonify
19
+ from flask_cors import CORS
20
+
21
+ import config
22
+ from intake.routes import intake_bp
23
+ from radiology.routes import radiology_bp
24
+
25
+ # Configure logging
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
29
+ handlers=[logging.StreamHandler()],
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Initialize Flask app
34
+ # Point static_folder to the React build output directory
35
+ app = Flask(
36
+ __name__,
37
+ static_folder=str(config.FRONTEND_BUILD),
38
+ static_url_path="",
39
+ )
40
+
41
+ # Enable CORS for all API endpoints
42
+ CORS(app, resources={r"/api/*": {"origins": "*"}})
43
+
44
+ # Register blueprints
45
+ app.register_blueprint(intake_bp, url_prefix="/api/intake")
46
+ app.register_blueprint(radiology_bp, url_prefix="/api/radiology")
47
+
48
+
49
+ # ── Serve Radiology static assets ───────────────────────────────────────────
50
+ @app.route("/static/images/<path:filename>")
51
+ def serve_radiology_images(filename):
52
+ """Serve radiology images from base static directory."""
53
+ static_images_dir = config.BASE_DIR / "static" / "images"
54
+ return send_from_directory(str(static_images_dir), filename)
55
+
56
+
57
+ @app.route("/static/reports/<path:filename>")
58
+ def serve_radiology_reports(filename):
59
+ """Serve raw radiology reports from base static directory."""
60
+ static_reports_dir = config.BASE_DIR / "static" / "reports"
61
+ return send_from_directory(str(static_reports_dir), filename)
62
+
63
+
64
+ # ── Health Check ────────────────────────────────────────────────────────────
65
+ @app.route("/api/health", methods=["GET"])
66
+ def health_check():
67
+ """Health check endpoint."""
68
+ return jsonify({
69
+ "status": "healthy",
70
+ "app": "Sushruta Patient 360",
71
+ "medgemma_27b_initialized": bool(config.MEDGEMMA_27B_ENDPOINT or config.HF_TOKEN),
72
+ "medgemma_4b_initialized": bool(config.MEDGEMMA_4B_ENDPOINT or config.HF_TOKEN),
73
+ })
74
+
75
+
76
+ # ── Serve React Frontend ────────────────────────────────────────────────────
77
+ @app.route("/", defaults={"path": ""})
78
+ @app.route("/<path:path>")
79
+ def serve_react(path):
80
+ """Serve the index.html or assets from the React build folder."""
81
+ static_folder = str(config.FRONTEND_BUILD)
82
+
83
+ # If file exists in React build folder, serve it
84
+ if path and os.path.exists(os.path.join(static_folder, path)):
85
+ return send_from_directory(static_folder, path)
86
+
87
+ # Fallback to serving index.html for React SPA routing
88
+ return send_from_directory(static_folder, "index.html")
89
+
90
+
91
+ if __name__ == "__main__":
92
+ port = int(os.environ.get("PORT", 7860))
93
+ logger.info("Starting Sushruta Patient 360 server on port %d...", port)
94
+ app.run(host="0.0.0.0", port=port, debug=False)
appoint-ready ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 88024fb7791429f7a5acf5668b904d23d6fcc89d
cache_manager.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Cache manager for Sushruta Patient 360 using diskcache."""
15
+
16
+ import logging
17
+ import shutil
18
+ from pathlib import Path
19
+ from diskcache import Cache
20
+ from config import CACHE_DIR
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Ensure CACHE_DIR exists
25
+ try:
26
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
27
+ except Exception as e:
28
+ logger.warning("Could not create CACHE_DIR %s: %s. Using local ./cache instead.", CACHE_DIR, e)
29
+ CACHE_DIR = Path(__file__).resolve().parent / "cache"
30
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
31
+
32
+ # Define Cache instances
33
+ intake_cache = Cache(str(CACHE_DIR / "intake"))
34
+ radiology_cache = Cache(str(CACHE_DIR / "radiology"))
35
+
36
+ logger.info("Initialized diskcache at: %s", CACHE_DIR)
37
+
38
+
39
+ def create_cache_zip(archive_path_prefix: str) -> str:
40
+ """Create a zip archive of the CACHE_DIR and return the path to the zip file.
41
+
42
+ Saves it as `archive_path_prefix.zip`.
43
+ """
44
+ zip_path = shutil.make_archive(archive_path_prefix, "zip", str(CACHE_DIR))
45
+ logger.info("Created cache archive zip at: %s", zip_path)
46
+ return zip_path
config.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Unified configuration for Sushruta Patient 360."""
15
+
16
+ import csv
17
+ import logging
18
+ import os
19
+ from pathlib import Path
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # ── Base paths ──────────────────────────────────────────────────────────────
24
+ BASE_DIR = Path(__file__).resolve().parent
25
+
26
+ # ── Environment variables ───────────────────────────────────────────────────
27
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
28
+ MEDGEMMA_27B_ENDPOINT = os.environ.get("MEDGEMMA_27B_ENDPOINT", "")
29
+ MEDGEMMA_4B_ENDPOINT = os.environ.get("MEDGEMMA_4B_ENDPOINT", "")
30
+ GENERATE_SPEECH = os.environ.get("GENERATE_SPEECH", "false").lower() in (
31
+ "true",
32
+ "1",
33
+ "yes",
34
+ )
35
+ CACHE_DIR = Path(os.environ.get("CACHE_DIR", "/cache"))
36
+ FRONTEND_BUILD = Path(
37
+ os.environ.get("FRONTEND_BUILD", str(BASE_DIR / "frontend" / "build"))
38
+ )
39
+
40
+ # ── Radiology report manifest ──────────────────────────────────────────────
41
+ AVAILABLE_REPORTS: dict[str, dict] = {}
42
+
43
+
44
+ def load_reports_manifest() -> dict[str, dict]:
45
+ """Load the radiology reports manifest from static/reports_manifest.csv.
46
+
47
+ CSV columns: image_type, case_display_name, image_path, report_path
48
+ Returns a dict keyed by case_display_name.
49
+ """
50
+ manifest_path = BASE_DIR / "static" / "reports_manifest.csv"
51
+ reports: dict[str, dict] = {}
52
+
53
+ if not manifest_path.exists():
54
+ logger.warning("Reports manifest not found at %s", manifest_path)
55
+ return reports
56
+
57
+ with open(manifest_path, newline="", encoding="utf-8") as f:
58
+ reader = csv.DictReader(f)
59
+ for row in reader:
60
+ image_type = row.get("image_type", "").strip()
61
+ case_name = row.get("case_display_name", "").strip()
62
+ image_path = row.get("image_path", "").strip()
63
+ report_path = row.get("report_path", "").strip()
64
+
65
+ if not case_name:
66
+ continue
67
+
68
+ # Validate file paths exist (relative to BASE_DIR)
69
+ abs_image = BASE_DIR / image_path
70
+ abs_report = BASE_DIR / report_path
71
+
72
+ if not abs_image.exists():
73
+ logger.warning(
74
+ "Image file not found for report '%s': %s",
75
+ case_name,
76
+ abs_image,
77
+ )
78
+ if not abs_report.exists():
79
+ logger.warning(
80
+ "Report file not found for report '%s': %s",
81
+ case_name,
82
+ abs_report,
83
+ )
84
+
85
+ reports[case_name] = {
86
+ "image_type": image_type,
87
+ "case_display_name": case_name,
88
+ "image_path": image_path,
89
+ "report_path": report_path,
90
+ }
91
+
92
+ logger.info("Loaded %d radiology reports from manifest", len(reports))
93
+ return reports
94
+
95
+
96
+ # Load manifest at import time
97
+ AVAILABLE_REPORTS = load_reports_manifest()
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "diff": "^8.0.2",
7
+ "html-react-parser": "^5.2.5",
8
+ "marked": "^15.0.12",
9
+ "react": "^18.2.0",
10
+ "react-dom": "^18.2.0",
11
+ "react-scripts": "^5.0.1",
12
+ "@textea/json-viewer": "^3.2.1",
13
+ "@mui/material": "^5.15.20"
14
+ },
15
+ "scripts": {
16
+ "start": "cross-env NODE_OPTIONS=--openssl-legacy-provider react-scripts start",
17
+ "build": "react-scripts build"
18
+ },
19
+ "browserslist": {
20
+ "production": [
21
+ ">0.2%",
22
+ "not dead",
23
+ "not op_mini all"
24
+ ],
25
+ "development": [
26
+ "last 1 chrome version",
27
+ "last 1 firefox version",
28
+ "last 1 safari version"
29
+ ]
30
+ }
31
+ }
frontend/public/assets/ai_headshot.svg ADDED
frontend/public/assets/alex.avif ADDED

Git LFS Details

  • SHA256: 319a0d71ac39ef5109708c4be044e3d80b53eb90584ff8946a7b28b8b8a4ff42
  • Pointer size: 130 Bytes
  • Size of remote file: 14.5 kB
frontend/public/assets/alex.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ae46c52980e7d7a176245a8f42e90c7386af502e2efed87721937a0e3c9e53ae
3
+ size 1122871
frontend/public/assets/alex_300.avif ADDED

Git LFS Details

  • SHA256: 94a4d7c05adabf9645d9b926aedaf823b0dcbd144690eda6aa2f49b6908c23ad
  • Pointer size: 129 Bytes
  • Size of remote file: 2.99 kB
frontend/public/assets/alex_fhir.json ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "resourceType": "Patient",
4
+ "id": "alex-sharma-63-female",
5
+ "meta": {
6
+ "profile": [
7
+ "http://hl7.org/fhir/R4/StructureDefinition/Patient"
8
+ ]
9
+ },
10
+ "text": {
11
+ "status": "generated",
12
+ "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Alex Sharma</b> (Female, 63)</p><p>Known Conditions: Diabetes</p></div>"
13
+ },
14
+ "identifier": [
15
+ {
16
+ "use": "usual",
17
+ "type": {
18
+ "coding": [
19
+ {
20
+ "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
21
+ "code": "MR",
22
+ "display": "Medical record number"
23
+ }
24
+ ]
25
+ },
26
+ "system": "http://example.org/patients",
27
+ "value": "PAT-2023-001"
28
+ }
29
+ ],
30
+ "name": [
31
+ {
32
+ "use": "official",
33
+ "family": "Sharma",
34
+ "given": [
35
+ "Alex"
36
+ ]
37
+ }
38
+ ],
39
+ "gender": "female",
40
+ "birthDate": "1962-01-15",
41
+ "deceasedBoolean": false
42
+ },
43
+ {
44
+ "resourceType": "Encounter",
45
+ "id": "encounter-diabetes-followup",
46
+ "meta": {
47
+ "profile": [
48
+ "http://hl7.org/fhir/R4/StructureDefinition/Encounter"
49
+ ]
50
+ },
51
+ "text": {
52
+ "status": "generated",
53
+ "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Diabetes Follow-up Visit</b> for Alex Sharma on 2024-03-10</p></div>"
54
+ },
55
+ "status": "finished",
56
+ "class": {
57
+ "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
58
+ "code": "AMB",
59
+ "display": "Ambulatory"
60
+ },
61
+ "type": [
62
+ {
63
+ "coding": [
64
+ {
65
+ "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
66
+ "code": "FLD",
67
+ "display": "Field"
68
+ }
69
+ ],
70
+ "text": "Follow-up visit for Diabetes"
71
+ }
72
+ ],
73
+ "subject": {
74
+ "reference": "Patient/alex-sharma-63-female",
75
+ "display": "Alex Sharma"
76
+ },
77
+ "period": {
78
+ "start": "2024-03-10T10:00:00Z",
79
+ "end": "2024-03-10T10:45:00Z"
80
+ },
81
+ "serviceProvider": {
82
+ "reference": "Organization/example-org",
83
+ "display": "Example Medical Center"
84
+ }
85
+ },
86
+ {
87
+ "resourceType": "Condition",
88
+ "id": "condition-diabetes-mellitus",
89
+ "meta": {
90
+ "profile": [
91
+ "http://hl7.org/fhir/R4/StructureDefinition/Condition"
92
+ ]
93
+ },
94
+ "text": {
95
+ "status": "generated",
96
+ "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Diabetes Mellitus, Type 2</b> for Alex Sharma, diagnosed 2020-05-20</p></div>"
97
+ },
98
+ "clinicalStatus": {
99
+ "coding": [
100
+ {
101
+ "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
102
+ "code": "active",
103
+ "display": "Active"
104
+ }
105
+ ]
106
+ },
107
+ "verificationStatus": {
108
+ "coding": [
109
+ {
110
+ "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
111
+ "code": "confirmed",
112
+ "display": "Confirmed"
113
+ }
114
+ ]
115
+ },
116
+ "category": [
117
+ {
118
+ "coding": [
119
+ {
120
+ "system": "http://terminology.hl7.org/CodeSystem/condition-category",
121
+ "code": "problem-list-item",
122
+ "display": "Problem List Item"
123
+ }
124
+ ]
125
+ }
126
+ ],
127
+ "severity": {
128
+ "coding": [
129
+ {
130
+ "system": "http://terminology.hl7.org/CodeSystem/condition-severity",
131
+ "code": "24484000",
132
+ "display": "Moderate"
133
+ }
134
+ ]
135
+ },
136
+ "code": {
137
+ "coding": [
138
+ {
139
+ "system": "http://snomed.info/sct",
140
+ "code": "44054006",
141
+ "display": "Diabetes mellitus"
142
+ }
143
+ ],
144
+ "text": "Diabetes Mellitus, Type 2"
145
+ },
146
+ "subject": {
147
+ "reference": "Patient/alex-sharma-63-female",
148
+ "display": "Alex Sharma"
149
+ },
150
+ "onsetDateTime": "2020-05-20"
151
+ },
152
+ {
153
+ "resourceType": "MedicationRequest",
154
+ "id": "medicationrequest-metformin",
155
+ "meta": {
156
+ "profile": [
157
+ "http://hl7.org/fhir/R4/StructureDefinition/MedicationRequest"
158
+ ]
159
+ },
160
+ "text": {
161
+ "status": "generated",
162
+ "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Metformin 500mg</b> for Alex Sharma, 1 tablet by mouth twice daily, ongoing</p></div>"
163
+ },
164
+ "status": "active",
165
+ "intent": "order",
166
+ "medicationCodeableConcept": {
167
+ "coding": [
168
+ {
169
+ "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
170
+ "code": "6809",
171
+ "display": "Metformin"
172
+ }
173
+ ],
174
+ "text": "Metformin 500mg Tablet"
175
+ },
176
+ "subject": {
177
+ "reference": "Patient/alex-sharma-63-female",
178
+ "display": "Alex Sharma"
179
+ },
180
+ "encounter": {
181
+ "reference": "Encounter/encounter-diabetes-followup",
182
+ "display": "Diabetes Follow-up Visit"
183
+ },
184
+ "authoredOn": "2024-03-10T10:30:00Z",
185
+ "requester": {
186
+ "reference": "Practitioner/dr-smith",
187
+ "display": "Dr. Jane Smith"
188
+ },
189
+ "dosageInstruction": [
190
+ {
191
+ "sequence": 1,
192
+ "text": "One tablet by mouth twice daily",
193
+ "timing": {
194
+ "repeat": {
195
+ "frequency": 2,
196
+ "period": 1,
197
+ "periodUnit": "d"
198
+ }
199
+ },
200
+ "route": {
201
+ "coding": [
202
+ {
203
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RouteOfAdministration",
204
+ "code": "PO",
205
+ "display": "Oral"
206
+ }
207
+ ]
208
+ },
209
+ "doseAndRate": [
210
+ {
211
+ "type": {
212
+ "coding": [
213
+ {
214
+ "system": "http://terminology.hl7.org/CodeSystem/dose-rate-type",
215
+ "code": "ordered",
216
+ "display": "Ordered"
217
+ }
218
+ ]
219
+ },
220
+ "doseQuantity": {
221
+ "value": 1,
222
+ "unit": "tablet",
223
+ "system": "http://unitsofmeasure.org",
224
+ "code": "{tablet}"
225
+ }
226
+ }
227
+ ]
228
+ }
229
+ ],
230
+ "dispenseRequest": {
231
+ "numberOfRepeatsAllowed": 3,
232
+ "quantity": {
233
+ "value": 60,
234
+ "unit": "tablet",
235
+ "system": "http://unitsofmeasure.org",
236
+ "code": "{tablet}"
237
+ },
238
+ "expectedSupplyDuration": {
239
+ "value": 30,
240
+ "unit": "days",
241
+ "system": "http://unitsofmeasure.org",
242
+ "code": "d"
243
+ }
244
+ }
245
+ }
246
+ ]
frontend/public/assets/gemini.avif ADDED

Git LFS Details

  • SHA256: 00dd38be4386d7ef74af3e9ca082de54a2d7dd62f997f8c8ae9e152a7ca356b1
  • Pointer size: 130 Bytes
  • Size of remote file: 57.3 kB
frontend/public/assets/jason_fhir.json ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "resourceType": "Bundle",
3
+ "id": "Jordon-Dubois-Depression-Prozac-Encounter",
4
+ "type": "collection",
5
+ "entry": [
6
+ {
7
+ "fullUrl": "urn:uuid:patient-jordon-dubois",
8
+ "resource": {
9
+ "resourceType": "Patient",
10
+ "id": "jordon-dubois",
11
+ "name": [
12
+ {
13
+ "given": ["Jordon"],
14
+ "family": "Dubois"
15
+ }
16
+ ],
17
+ "gender": "male",
18
+ "birthDate": "1990-06-11"
19
+ }
20
+ },
21
+ {
22
+ "fullUrl": "urn:uuid:condition-depression",
23
+ "resource": {
24
+ "resourceType": "Condition",
25
+ "id": "depression-jordon-dubois",
26
+ "subject": {
27
+ "reference": "urn:uuid:patient-jordon-dubois"
28
+ },
29
+ "code": {
30
+ "coding": [
31
+ {
32
+ "system": "http://snomed.info/sct",
33
+ "code": "366053000",
34
+ "display": "Depressive disorder"
35
+ }
36
+ ],
37
+ "text": "Depression"
38
+ },
39
+ "clinicalStatus": {
40
+ "coding": [
41
+ {
42
+ "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
43
+ "code": "active",
44
+ "display": "Active"
45
+ }
46
+ ]
47
+ },
48
+ "verificationStatus": {
49
+ "coding": [
50
+ {
51
+ "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
52
+ "code": "confirmed",
53
+ "display": "Confirmed"
54
+ }
55
+ ]
56
+ },
57
+ "recordedDate": "2024-01-15T10:00:00Z"
58
+ }
59
+ },
60
+ {
61
+ "fullUrl": "urn:uuid:medication-prozac",
62
+ "resource": {
63
+ "resourceType": "Medication",
64
+ "id": "prozac",
65
+ "code": {
66
+ "coding": [
67
+ {
68
+ "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
69
+ "code": "100371",
70
+ "display": "Fluoxetine"
71
+ }
72
+ ],
73
+ "text": "Prozac"
74
+ }
75
+ }
76
+ },
77
+ {
78
+ "fullUrl": "urn:uuid:medicationrequest-prozac",
79
+ "resource": {
80
+ "resourceType": "MedicationRequest",
81
+ "id": "prozac-request-jordon-dubois",
82
+ "subject": {
83
+ "reference": "urn:uuid:patient-jordon-dubois"
84
+ },
85
+ "medicationReference": {
86
+ "reference": "urn:uuid:medication-prozac"
87
+ },
88
+ "requester": {
89
+ "display": "Dr. Smith"
90
+ },
91
+ "authoredOn": "2024-01-15T11:00:00Z",
92
+ "status": "active",
93
+ "intent": "order",
94
+ "dosageInstruction": [
95
+ {
96
+ "text": "20mg daily",
97
+ "timing": {
98
+ "repeat": {
99
+ "frequency": 1,
100
+ "period": 1,
101
+ "periodUnit": "d"
102
+ }
103
+ },
104
+ "route": {
105
+ "coding": [
106
+ {
107
+ "system": "http://terminology.hl7.org/CodeSystem/v3-RouteOfAdministration",
108
+ "code": "PO",
109
+ "display": "Oral"
110
+ }
111
+ ]
112
+ },
113
+ "doseAndRate": [
114
+ {
115
+ "type": {
116
+ "coding": [
117
+ {
118
+ "system": "http://terminology.hl7.org/CodeSystem/dose-rate-type",
119
+ "code": "ordered",
120
+ "display": "Ordered"
121
+ }
122
+ ]
123
+ },
124
+ "doseQuantity": {
125
+ "value": 20,
126
+ "unit": "mg",
127
+ "system": "http://unitsofmeasure.org",
128
+ "code": "mg"
129
+ }
130
+ }
131
+ ]
132
+ }
133
+ ],
134
+ "reasonReference": [
135
+ {
136
+ "reference": "urn:uuid:condition-depression"
137
+ }
138
+ ]
139
+ }
140
+ },
141
+ {
142
+ "fullUrl": "urn:uuid:encounter-depression",
143
+ "resource": {
144
+ "resourceType": "Encounter",
145
+ "id": "encounter-jordon-dubois-depression",
146
+ "status": "finished",
147
+ "class": {
148
+ "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
149
+ "code": "AMB",
150
+ "display": "Ambulatory"
151
+ },
152
+ "subject": {
153
+ "reference": "urn:uuid:patient-jordon-dubois"
154
+ },
155
+ "period": {
156
+ "start": "2024-01-15T09:30:00Z",
157
+ "end": "2024-01-15T10:30:00Z"
158
+ },
159
+ "reasonReference": [
160
+ {
161
+ "reference": "urn:uuid:condition-depression"
162
+ }
163
+ ]
164
+ }
165
+ }
166
+ ]
167
+ }
frontend/public/assets/jordan.avif ADDED

Git LFS Details

  • SHA256: d523fe0d37ac1a8523808beeef5b92b7eeb0b66d04e31e14825d01b8cb1bb357
  • Pointer size: 130 Bytes
  • Size of remote file: 24.3 kB
frontend/public/assets/jordan.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a9774a57078508dbcd1626463b1deb84bd468d7b81111b568a966ef3baa56051
3
+ size 1248841
frontend/public/assets/jordan_300.avif ADDED

Git LFS Details

  • SHA256: e6b63b7aabecec5f42d0fa25019465dc90fdc362b82a5a921cac9408f4cc0649
  • Pointer size: 129 Bytes
  • Size of remote file: 4.87 kB
frontend/public/assets/medgemma.avif ADDED

Git LFS Details

  • SHA256: 65c47f50fe13b9045f562ccb6702f0d22f9e03600882063d9f00bbb625a78392
  • Pointer size: 129 Bytes
  • Size of remote file: 5.48 kB
frontend/public/assets/patients_and_conditions.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "patients": [
3
+ {
4
+ "id": 1,
5
+ "name": "Jordon Dubois",
6
+ "gender": "Male",
7
+ "age": 35,
8
+ "existing_condition": "Depression",
9
+ "img": "/assets/jordan.avif",
10
+ "video": "/assets/jordan.mp4",
11
+ "headshot": "/assets/jordan_300.avif",
12
+ "fhirFile": "/assets/jason_fhir.json",
13
+ "voice": "Algenib"
14
+ },
15
+ {
16
+ "id": 2,
17
+ "name": "Alex Sharma",
18
+ "gender": "Female",
19
+ "age": 63,
20
+ "existing_condition": "Diabetes",
21
+ "img": "/assets/alex.avif",
22
+ "video": "/assets/alex.mp4",
23
+ "headshot": "/assets/alex_300.avif",
24
+ "fhirFile": "/assets/alex_fhir.json",
25
+ "voice": "Gacrux"
26
+ },
27
+ {
28
+ "id": 3,
29
+ "name": "Sacha Silva",
30
+ "gender": "Female",
31
+ "age": 24,
32
+ "existing_condition": "Asthma",
33
+ "img": "/assets/sacha.avif",
34
+ "video": "/assets/sacha.mp4",
35
+ "headshot": "/assets/sacha_150.avif",
36
+ "fhirFile": "/assets/sacha_fhir.json",
37
+ "voice": "Callirrhoe"
38
+ }
39
+ ],
40
+ "conditions": [
41
+ { "name": "Flu", "description": "A common and contagious respiratory illness caused by a virus that can lead to fever, body aches, and fatigue." },
42
+ { "name": "Malaria", "description": "A serious disease spread by mosquitoes that causes recurring fevers and chills due to a parasite infecting red blood cells." },
43
+ { "name": "Migraine", "description": "A type of severe headache often accompanied by throbbing pain, sensitivity to light and sound, and sometimes nausea." },
44
+ { "name": "Serotonin Syndrome", "description": "A potentially dangerous reaction caused by too much serotonin in the brain, often due to certain medications, leading to symptoms like agitation, rapid heart rate, and confusion." }
45
+ ]
46
+ }
frontend/public/assets/sacha.avif ADDED

Git LFS Details

  • SHA256: d394fadf538804a99714d04650ab209e7fb18ea39506a8bd71097dd9572ccc02
  • Pointer size: 129 Bytes
  • Size of remote file: 5.98 kB
frontend/public/assets/sacha.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c722df889fafc7646d27e8fc9d15c279df43a0be68991f122ed17e096745a867
3
+ size 751974
frontend/public/assets/sacha_150.avif ADDED

Git LFS Details

  • SHA256: 01485673c40040788efb14c25850fdc83cc8925cc44d3d195c6bfbd02fbca753
  • Pointer size: 129 Bytes
  • Size of remote file: 1.85 kB
frontend/public/assets/sacha_fhir.json ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "resourceType": "Bundle",
3
+ "type": "collection",
4
+ "entry": [
5
+ {
6
+ "resource": {
7
+ "resourceType": "Patient",
8
+ "id": "sacha-silva-patient",
9
+ "identifier": [
10
+ {
11
+ "system": "http://example.org/mrn",
12
+ "value": "1234567"
13
+ },
14
+ {
15
+ "system": "http://hl7.org/fhir/sid/us-ssn",
16
+ "value": "999-88-7777"
17
+ }
18
+ ],
19
+ "name": [
20
+ {
21
+ "family": "Silva",
22
+ "given": [
23
+ "Sacha"
24
+ ]
25
+ }
26
+ ],
27
+ "gender": "female",
28
+ "birthDate": "1993-10-27",
29
+ "address": [
30
+ {
31
+ "line": [
32
+ "123 Main Street"
33
+ ],
34
+ "city": "Anytown",
35
+ "state": "CA",
36
+ "postalCode": "91234",
37
+ "country": "US"
38
+ }
39
+ ],
40
+ "telecom": [
41
+ {
42
+ "system": "phone",
43
+ "value": "555-123-4567",
44
+ "use": "home"
45
+ },
46
+ {
47
+ "system": "email",
48
+ "value": "sacha.silva@example.com",
49
+ "use": "home"
50
+ }
51
+ ]
52
+ },
53
+ "request": {
54
+ "method": "PUT",
55
+ "url": "Patient/sacha-silva-patient"
56
+ }
57
+ },
58
+ {
59
+ "resource": {
60
+ "resourceType": "Condition",
61
+ "id": "asthma-condition",
62
+ "clinicalStatus": {
63
+ "coding": [
64
+ {
65
+ "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
66
+ "code": "active",
67
+ "display": "Active"
68
+ }
69
+ ]
70
+ },
71
+ "verificationStatus": {
72
+ "coding": [
73
+ {
74
+ "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
75
+ "code": "confirmed",
76
+ "display": "Confirmed"
77
+ }
78
+ ]
79
+ },
80
+ "category": [
81
+ {
82
+ "coding": [
83
+ {
84
+ "system": "http://terminology.hl7.org/CodeSystem/condition-category",
85
+ "code": "problem-list-item",
86
+ "display": "Problem List Item"
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "code": {
92
+ "coding": [
93
+ {
94
+ "system": "http://snomed.info/sct",
95
+ "code": "195967001",
96
+ "display": "Asthma"
97
+ }
98
+ ],
99
+ "text": "Asthma"
100
+ },
101
+ "subject": {
102
+ "reference": "Patient/sacha-silva-patient",
103
+ "display": "Sacha Silva"
104
+ },
105
+ "onsetDateTime": "2010-05-15"
106
+ },
107
+ "request": {
108
+ "method": "PUT",
109
+ "url": "Condition/asthma-condition"
110
+ }
111
+ },
112
+ {
113
+ "resource": {
114
+ "resourceType": "Encounter",
115
+ "id": "asthma-encounter-1",
116
+ "status": "finished",
117
+ "class": {
118
+ "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
119
+ "code": "AMB",
120
+ "display": "Ambulatory"
121
+ },
122
+ "type": [
123
+ {
124
+ "coding": [
125
+ {
126
+ "system": "http://snomed.info/sct",
127
+ "code": "308335008",
128
+ "display": "Patient encounter procedure"
129
+ }
130
+ ],
131
+ "text": "Asthma Follow-up"
132
+ }
133
+ ],
134
+ "subject": {
135
+ "reference": "Patient/sacha-silva-patient",
136
+ "display": "Sacha Silva"
137
+ },
138
+ "period": {
139
+ "start": "2023-08-10T10:00:00-07:00",
140
+ "end": "2023-08-10T10:30:00-07:00"
141
+ },
142
+ "reasonCode": [
143
+ {
144
+ "coding": [
145
+ {
146
+ "system": "http://snomed.info/sct",
147
+ "code": "266253002",
148
+ "display": "Asthma exacerbation"
149
+ }
150
+ ],
151
+ "text": "Asthma Exacerbation"
152
+ }
153
+ ]
154
+ },
155
+ "request": {
156
+ "method": "PUT",
157
+ "url": "Encounter/asthma-encounter-1"
158
+ }
159
+ },
160
+ {
161
+ "resource": {
162
+ "resourceType": "MedicationRequest",
163
+ "id": "albuterol-mr",
164
+ "status": "active",
165
+ "intent": "order",
166
+ "medicationCodeableConcept": {
167
+ "coding": [
168
+ {
169
+ "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
170
+ "code": "207182",
171
+ "display": "Albuterol 90 mcg/actuation Metered Dose Inhaler"
172
+ }
173
+ ],
174
+ "text": "Albuterol Inhaler"
175
+ },
176
+ "subject": {
177
+ "reference": "Patient/sacha-silva-patient",
178
+ "display": "Sacha Silva"
179
+ },
180
+ "encounter": {
181
+ "reference": "Encounter/asthma-encounter-1",
182
+ "display": "Asthma Follow-up"
183
+ },
184
+ "authoredOn": "2023-08-10T10:30:00-07:00",
185
+ "requester": {
186
+ "reference": "Practitioner/dr-jane-doe",
187
+ "display": "Jane Doe, MD"
188
+ },
189
+ "dosageInstruction": [
190
+ {
191
+ "sequence": 1,
192
+ "text": "2 puffs every 4-6 hours as needed for wheezing",
193
+ "timing": {
194
+ "repeat": {
195
+ "frequency": 4,
196
+ "period": 1,
197
+ "periodUnit": "h"
198
+ }
199
+ },
200
+ "doseQuantity": {
201
+ "value": 2,
202
+ "unit": "puff",
203
+ "system": "http://unitsofmeasure.org",
204
+ "code": "{puff}"
205
+ },
206
+ "asNeededCodeableConcept": {
207
+ "coding": [
208
+ {
209
+ "system": "http://snomed.info/sct",
210
+ "code": "267036007",
211
+ "display": "Wheezing"
212
+ }
213
+ ],
214
+ "text": "Wheezing"
215
+ }
216
+ }
217
+ ],
218
+ "dispenseRequest": {
219
+ "quantity": {
220
+ "value": 1,
221
+ "unit": "inhaler",
222
+ "system": "http://terminology.hl7.org/CodeSystem/v3-orderableDrugForm",
223
+ "code": "INH"
224
+ },
225
+ "expectedSupplyDuration": {
226
+ "value": 30,
227
+ "unit": "days",
228
+ "system": "http://unitsofmeasure.org",
229
+ "code": "d"
230
+ }
231
+ }
232
+ },
233
+ "request": {
234
+ "method": "PUT",
235
+ "url": "MedicationRequest/albuterol-mr"
236
+ }
237
+ },
238
+ {
239
+ "resource": {
240
+ "resourceType": "Practitioner",
241
+ "id": "dr-jane-doe",
242
+ "name": [
243
+ {
244
+ "family": "Doe",
245
+ "given": [
246
+ "Jane"
247
+ ],
248
+ "prefix": [
249
+ "Dr."
250
+ ]
251
+ }
252
+ ],
253
+ "identifier": [
254
+ {
255
+ "system": "http://example.org/npi",
256
+ "value": "1234567890"
257
+ }
258
+ ]
259
+ },
260
+ "request": {
261
+ "method": "PUT",
262
+ "url": "Practitioner/dr-jane-doe"
263
+ }
264
+ }
265
+ ]
266
+ }
frontend/public/assets/welcome_bottom_graphics.svg ADDED
frontend/public/assets/welcome_graphics.svg ADDED
frontend/public/assets/welcome_top_graphics.svg ADDED
frontend/public/index.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Copyright 2025 Google LLC
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ -->
17
+
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="utf-8" />
21
+ <title>Sushruta — Patient 360</title>
22
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+ <meta name="description" content="Sushruta Patient 360 Demo - Powered by MedGemma. Simulated pre-visit intake clinical dialogues and radiology report plain-language explanations." />
24
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
25
+ </head>
26
+ <body>
27
+ <noscript>You need to enable JavaScript to run this app.</noscript>
28
+ <div id="root"></div>
29
+ </body>
30
+ </html>
frontend/src/App.js ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React, { useState } from 'react';
18
+ import Landing from './components/Landing/Landing';
19
+ import WelcomePage from './components/WelcomePage/WelcomePage';
20
+ import PatientBuilder from './components/PatientBuilder/PatientBuilder';
21
+ import RolePlayDialogs from './components/RolePlayDialogs/RolePlayDialogs';
22
+ import Interview from './components/Interview/Interview';
23
+ import RadiologyExplainer from './components/RadiologyExplainer/RadiologyExplainer';
24
+ import PreloadImages from './components/PreloadImages';
25
+
26
+ const App = () => {
27
+ const [currentPage, setCurrentPage] = useState('landing');
28
+ const [selectedPatient, setSelectedPatient] = useState(null);
29
+ const [selectedCondition, setSelectedCondition] = useState(null);
30
+
31
+ const handleNavigate = (page) => {
32
+ if (page === 'previsit') {
33
+ setCurrentPage('welcome');
34
+ } else if (page === 'radiology') {
35
+ setCurrentPage('radiology');
36
+ }
37
+ };
38
+
39
+ const handleSwitchPage = () => {
40
+ setCurrentPage('patientBuilder');
41
+ };
42
+
43
+ const handleSwitchToRolePlayDialogs = () => {
44
+ setCurrentPage('rolePlayDialogs');
45
+ };
46
+
47
+ const handleSwitchToInterview = () => {
48
+ setCurrentPage('interview');
49
+ };
50
+
51
+ const imageList = [
52
+ '/assets/gemini.avif',
53
+ '/assets/medgemma.avif',
54
+ '/assets/ai_headshot.svg',
55
+ '/assets/jordan_300.avif',
56
+ '/assets/alex_300.avif',
57
+ '/assets/sacha_150.avif',
58
+ '/assets/jordan.avif',
59
+ '/assets/alex.avif',
60
+ '/assets/sacha.avif'
61
+ ];
62
+
63
+ return (
64
+ <PreloadImages imageSources={imageList}>
65
+ {currentPage === 'landing' ? (
66
+ <Landing onNavigate={handleNavigate} />
67
+ ) : currentPage === 'radiology' ? (
68
+ <RadiologyExplainer onBack={() => setCurrentPage('landing')} />
69
+ ) : currentPage === 'welcome' ? (
70
+ <WelcomePage
71
+ onSwitchPage={handleSwitchPage}
72
+ setSelectedPatient={setSelectedPatient}
73
+ setSelectedCondition={setSelectedCondition}
74
+ onBack={() => setCurrentPage('landing')}
75
+ />
76
+ ) : currentPage === 'patientBuilder' ? (
77
+ <PatientBuilder
78
+ selectedPatient={selectedPatient}
79
+ selectedCondition={selectedCondition}
80
+ setSelectedPatient={setSelectedPatient}
81
+ setSelectedCondition={setSelectedCondition}
82
+ onNext={handleSwitchToRolePlayDialogs}
83
+ onBack={() => setCurrentPage('welcome')}
84
+ />
85
+ ) : currentPage === 'rolePlayDialogs' ? (
86
+ <RolePlayDialogs
87
+ selectedPatient={selectedPatient}
88
+ selectedCondition={selectedCondition}
89
+ onStart={handleSwitchToInterview}
90
+ onBack={() => setCurrentPage('patientBuilder')}
91
+ />
92
+ ) : currentPage === 'interview' ? (
93
+ <Interview
94
+ selectedPatient={selectedPatient}
95
+ selectedCondition={selectedCondition}
96
+ onBack={() => setCurrentPage('rolePlayDialogs')}
97
+ />
98
+ ) : null}
99
+ </PreloadImages>
100
+ );
101
+ };
102
+
103
+ export default App;
frontend/src/components/DetailsPopup/DetailsPopup.css ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ .popup-close-button {
18
+ position: absolute;
19
+ top: 10px;
20
+ right: 15px;
21
+ background: transparent;
22
+ border: none;
23
+ font-size: 24px;
24
+ cursor: pointer;
25
+ color: #888;
26
+ }
27
+
28
+ .popup-close-button:hover {
29
+ color: #000;
30
+ }
31
+
32
+ .details-popup-content h4 {
33
+ margin-top: 20px;
34
+ margin-bottom: 10px;
35
+ color: #333;
36
+ }
37
+
38
+ .details-popup-content ul {
39
+ list-style-type: none;
40
+ padding-left: 0;
41
+ }
42
+
43
+ .details-popup-content li {
44
+ margin-bottom: 8px;
45
+ color: #555;
46
+ }
frontend/src/components/DetailsPopup/DetailsPopup.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import './DetailsPopup.css';
19
+
20
+ const DetailsPopup = ({ isOpen, onClose }) => {
21
+ if (!isOpen) {
22
+ return null;
23
+ }
24
+
25
+ return (
26
+ <div className="popup-overlay" onClick={onClose}>
27
+ <div className="popup-content" onClick={(e) => e.stopPropagation()}>
28
+ <button className="popup-close-button" onClick={onClose}>&times;</button>
29
+ <h2 id="dialog-title" className="dialog-title-text">Details About This Demo</h2>
30
+ <p><b>The Model:</b> This demo features Google's MedGemma-27B, a Gemma 3-based model
31
+ fine-tuned for comprehending medical text. It demonstrates MedGemma's ability to
32
+ accelerate the development of AI-powered healthcare applications by offering advanced
33
+ interpretation of medical data.</p>
34
+ <p><b>Accessing and Using the Model:</b> Google's MedGemma-27B is available on <a
35
+ href="https://huggingface.co/google/medgemma-27b-text-it" target="_blank" rel="noopener noreferrer">HuggingFace<img
36
+ className="hf-logo"
37
+ src="https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.svg" />
38
+ </a> and is easily deployable via&nbsp;
39
+ <a href="https://console.cloud.google.com/vertex-ai/publishers/google/model-garden/medgemma" target="_blank" rel="noopener noreferrer">Model
40
+ Garden <img className="hf-logo"
41
+ src="https://www.gstatic.com/cloud/images/icons/apple-icon.png" /></a>.
42
+ Learn more about using the model and its limitations on the <a
43
+ href="https://developers.google.com/health-ai-developer-foundations?referral=appoint-ready"
44
+ target="_blank" rel="noopener noreferrer">HAI-DEF
45
+ developer site</a>.
46
+ </p>
47
+ <p><b>Health AI Developer Foundations (HAI-DEF)</b> provides a collection of open-weight models and
48
+ companion resources to empower developers in building AI models for healthcare.</p>
49
+ <p><b>Share this Demo:</b> If you find this demonstration valuable, we encourage you to share it on
50
+ social media.
51
+ <small>
52
+ &nbsp;<a href="https://www.linkedin.com/shareArticle?mini=true&url=https://huggingface.co/spaces/google/appoint-ready&text=%23MedGemma%20%23MedGemmaDemo" target="_blank" rel="noopener noreferrer">LinkedIn</a>
53
+ &nbsp;<a href="http://www.twitter.com/share?url=https://huggingface.co/spaces/google/appoint-ready&hashtags=MedGemma,MedGemmaDemo" target="_blank" rel="noopener noreferrer">X/Tweet</a>
54
+ </small>
55
+ </p>
56
+ <p><b>Explore More Demos:</b> Discover additional demonstrations on HuggingFace Spaces or via Colabs:
57
+ </p>
58
+ <ul>
59
+ <li><a href="https://huggingface.co/collections/google/hai-def-concept-apps-6837acfccce400abe6ec26c1"
60
+ target="_blank" rel="noopener noreferrer">
61
+ Collection of concept apps <img className="hf-logo" src="https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.svg" />
62
+ </a> built around HAI-DEF open models to inspire the community.</li>
63
+ <li><a href="https://github.com/Google-Health/medgemma/tree/main/notebooks/fine_tune_with_hugging_face.ipynb" target="_blank" rel="noopener noreferrer">
64
+ Finetune MedGemma Colab <img className="hf-logo"
65
+ src="https://upload.wikimedia.org/wikipedia/commons/d/d0/Google_Colaboratory_SVG_Logo.svg" /></a>
66
+ -
67
+ See an example of how to fine-tune this model.</li>
68
+ </ul>
69
+ For more technical details about this demo, please refer to the <a href="https://huggingface.co/spaces/google/appoint-ready/blob/main/README.md#table-of-contents" target="_blank" rel="noopener noreferrer">README</a> file in the repository.
70
+ <button className="popup-button" onClick={onClose}>Close</button>
71
+ </div>
72
+ </div>
73
+ );
74
+ };
75
+
76
+ export default DetailsPopup;
frontend/src/components/Interview/Interview.css ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+
18
+
19
+ .page.interview-page {
20
+ height: 100%;
21
+ max-height: 100%;
22
+ }
23
+
24
+ .interview-container {
25
+ padding: 20px;
26
+ font-family: Arial, sans-serif;
27
+ background-color: #f5f5f5;
28
+ }
29
+
30
+ .interview-split-container {
31
+ display: flex;
32
+ flex-direction: row;
33
+ height: 100%;
34
+ }
35
+
36
+ .interview-left-section {
37
+ border-right: 2px solid #e0e0e0;
38
+ display: flex;
39
+ flex-direction: row;
40
+ min-width: 550px;
41
+ max-width: 600px;
42
+ }
43
+
44
+ .toggle-icon {
45
+ vertical-align: text-top;
46
+ }
47
+
48
+ .interview-page .header2 span {
49
+ font-size: 14px;
50
+ font-weight: 100;
51
+ vertical-align: text-top;
52
+ animation: fadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
53
+ }
54
+
55
+ .interview-right-section {
56
+ display: flex;
57
+ flex-direction: column;
58
+ width: 60%;
59
+ min-width: 550px;
60
+ flex-grow: 2;
61
+ padding-left: 20px;
62
+ justify-content: space-between;
63
+ height: 100%;
64
+ gap: 5px;
65
+ }
66
+
67
+ .interview-header-panel {
68
+ flex: 0 0 320px;
69
+ display: flex;
70
+ flex-direction: column;
71
+ justify-content: flex-start;
72
+ padding: 32px 24px 0 0;
73
+ box-sizing: border-box;
74
+ background: #f5f5f5;
75
+ }
76
+
77
+ .interview-chat-panel {
78
+ flex: 1 1 0;
79
+ display: flex;
80
+ flex-direction: column;
81
+ justify-content: flex-start;
82
+ min-width: 0;
83
+ min-height: 0;
84
+ }
85
+
86
+ .chat-container {
87
+ flex: 1;
88
+ overflow-y: auto;
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: 10px;
92
+ width: 100%;
93
+ padding-right: 20px;
94
+ }
95
+
96
+ .chat-header {
97
+ display: flex;
98
+ justify-content: space-between;
99
+ gap: 10px;
100
+ margin-top: 20px;
101
+ margin: 20px 30px 0 30px;
102
+ width: -webkit-fill-available;
103
+ }
104
+
105
+ .chat-message-wrapper {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 10px;
109
+ }
110
+
111
+ /* Fade-in animation for new chat messages */
112
+ @keyframes fadeIn {
113
+ from { opacity: 0; }
114
+ to { opacity: 1; }
115
+ }
116
+
117
+ .chat-message-wrapper.fade-in {
118
+ animation: fadeIn 0.5s ease;
119
+ }
120
+
121
+ .chat-message-wrapper.patient {
122
+ align-self: end;
123
+ }
124
+
125
+ .chat-bubble {
126
+ padding: 10px 15px;
127
+ font-size: 16px;
128
+ line-height: 1.4;
129
+ flex: 1;
130
+
131
+ }
132
+
133
+ .patient .chat-bubble {
134
+ background-color: #eaeaea;
135
+ margin-right: 5px;
136
+ border-radius: 8px;
137
+ background: #F5F5F5;
138
+ }
139
+
140
+ .chat-avatar {
141
+ width: 30px;
142
+ height: 30px;
143
+ object-fit: cover;
144
+ border-radius: 50%;
145
+ background-color: #E8DEF8;
146
+ }
147
+
148
+ .interviewer .chat-avatar {
149
+ padding: 5px;
150
+ }
151
+
152
+ .patient .chat-avatar {
153
+ width: 40px;
154
+ height: 40px;
155
+ border-color: rgb(47, 95, 207);
156
+ }
157
+
158
+ .report-content {
159
+ padding: 20px;
160
+ overflow-y: auto;
161
+ flex: 1 1 0;
162
+ min-height: 0;
163
+ border-radius: 28px;
164
+ border: 2px solid #E9E9E9;
165
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
166
+ }
167
+
168
+ .report-content pre {
169
+ white-space: pre-wrap; /* CSS3 */
170
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
171
+ white-space: -pre-wrap; /* Opera 4-6 */
172
+ white-space: -o-pre-wrap; /* Opera 7 */
173
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
174
+ }
175
+
176
+ .thinking .chat-bubble {
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: 10px;
180
+ background-color: #E8DEF8;
181
+ padding: 20px;
182
+ border-radius: 8px;
183
+ min-width: 40px;
184
+ min-height: 40px;
185
+ position: relative;
186
+ color: #555;
187
+ border: none;
188
+ font-weight: 100;
189
+ }
190
+
191
+ .thinking-header {
192
+ font-weight: 500;
193
+ }
194
+
195
+ .chat-waiting-indicator {
196
+ color: #888;
197
+ font-size: 20px;
198
+ text-align: center;
199
+ margin: 60px 0;
200
+ font-style: italic;
201
+ opacity: 0.8;
202
+ }
203
+
204
+ .evaluate-button {
205
+ background-color: #C8B3FD;
206
+ color: #4E3B7B;
207
+ border-radius: 8px;
208
+ border-style: none;
209
+ padding: 6px;
210
+ font-size: 16px;
211
+ }
212
+
213
+ @keyframes fadeInOpacity {
214
+ 0% { opacity: 0; font-size: 0; }
215
+ 20% { opacity: 0; font-size: 1em; }
216
+ 100% { opacity: 1; font-size: 1em; }
217
+ }
218
+
219
+ /* New keyframes to unset text color after a delay */
220
+ @keyframes unsetColor {
221
+ to { color: unset; }
222
+ }
223
+
224
+ .add {
225
+ color: green;
226
+ animation: fadeInOpacity 1s forwards, unsetColor 0s forwards 5s;
227
+ }
228
+
229
+ @keyframes removeAnim {
230
+ 0% { opacity: 1; font-size: 1em; }
231
+ 80% { opacity: 0; font-size: 1em; }
232
+ 99% { font-size: 0.2em; }
233
+ 100% { opacity: 0; font-size: 0; display: none; }
234
+ }
235
+
236
+ .remove {
237
+ color: red;
238
+ text-decoration: line-through;
239
+ animation: removeAnim 1s forwards 5s;
240
+ }
241
+
242
+ .warning-icon {
243
+ color: #444746;
244
+ }
245
+
246
+ .disclaimer-container {
247
+ border-radius: 8px;
248
+ background: #FEF7E0;
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 20px;
252
+ padding: 13px;
253
+ font-size: 12px;
254
+ width: 100%;
255
+ }
256
+
257
+ .helpful {
258
+ border-radius: 14.272px;
259
+ background: #C4EED0;
260
+ mix-blend-mode: multiply;
261
+ display: inline-block;
262
+ padding: 0 5px;
263
+ }
264
+
265
+ .missing {
266
+ border-radius: 14.272px;
267
+ background: #FFE07C;
268
+ mix-blend-mode: multiply;
269
+ display: inline-block;
270
+ padding: 0 5px;
271
+ }
272
+
273
+ .evaluation-text {
274
+ font-style: italic;
275
+ padding-bottom: 30px;
276
+ }
277
+
278
+ .evaluation-text::after {
279
+ content: "***";
280
+ }
281
+
282
+ .volume-tooltip {
283
+ position: absolute;
284
+ bottom: 100%; /* Position above the icon */
285
+ left: 50%;
286
+ transform: translateX(-50%);
287
+ background-color: #333;
288
+ color: #fff;
289
+ padding: 5px 10px;
290
+ border-radius: 4px;
291
+ font-size: 0.8em;
292
+ white-space: nowrap;
293
+ z-index: 10;
294
+ margin-bottom: 5px; /* Space between icon and tooltip */
295
+ opacity: 0;
296
+ animation: fadeInOutTooltip 7s ease-in-out 15s forwards;
297
+ }
298
+
299
+ .volume-tooltip::after {
300
+ content: "";
301
+ position: absolute;
302
+ top: 100%;
303
+ left: 50%;
304
+ margin-left: -5px;
305
+ border-width: 5px;
306
+ border-style: solid;
307
+ border-color: #333 transparent transparent transparent;
308
+ }
309
+
310
+ @keyframes fadeInOutTooltip {
311
+ 0% {
312
+ opacity: 0;
313
+ }
314
+ 10% {
315
+ opacity: 1;
316
+ }
317
+ 90% {
318
+ opacity: 1;
319
+ }
320
+ 100% {
321
+ opacity: 0;
322
+ }
323
+ }
324
+
325
+
frontend/src/components/Interview/Interview.js ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React, { useState, useEffect, useRef } from "react";
18
+ import { marked } from "marked";
19
+ import parse from "html-react-parser";
20
+ import { diffArrays, diffWords } from "diff";
21
+ import "./Interview.css";
22
+ import DetailsPopup from "../DetailsPopup/DetailsPopup";
23
+
24
+ const Interview = ({ selectedPatient, selectedCondition, onBack }) => {
25
+ const [messages, setMessages] = useState([]);
26
+ const [isInterviewComplete, setIsInterviewComplete] = useState(false);
27
+ const [showEvaluation, setShowEvaluation] = useState(false);
28
+ const [isAudioEnabled, setIsAudioEnabled] = useState(true);
29
+ const [evaluation, setEvaluation] = useState('');
30
+ const [isFetchingEvaluation, setIsFetchingEvaluation] = useState(false);
31
+ const [currentReport, setCurrentReport] = useState("");
32
+ const [prevReport, setPrevReport] = useState("");
33
+ const [waitTime, setWaitTime] = useState(3000);
34
+ const [showEvaluationInfoPopup, setShowEvaluationInfoPopup] = useState(false);
35
+ const [isDetailsPopupOpen, setIsDetailsPopupOpen] = useState(false);
36
+ const chatContainerRef = useRef(null);
37
+ const reportContentRef = useRef(null);
38
+ const lastMessageRef = useRef(null);
39
+ const messageQueue = useRef([]);
40
+ const eventSourceRef = useRef(null);
41
+ const timeoutIdRef = useRef(null);
42
+
43
+ const currentPlayingAudio = useRef(null); // To keep track of the currently playing audio instance
44
+ const isAudioEnabledRef = useRef(isAudioEnabled);
45
+ useEffect(() => {
46
+ isAudioEnabledRef.current = isAudioEnabled;
47
+ }, [isAudioEnabled]);
48
+ const waitTimeRef = useRef(waitTime);
49
+ useEffect(() => {
50
+ waitTimeRef.current = waitTime;
51
+ }, [waitTime]);
52
+
53
+ const processQueue = React.useCallback(() => {
54
+ if (timeoutIdRef.current) {
55
+ clearTimeout(timeoutIdRef.current);
56
+ }
57
+
58
+ if (messageQueue.current.length === 0) {
59
+ // The queue is empty, so the processing chain for this batch is done.
60
+ // Clear the timeout ref so a new message can start a new chain.
61
+ timeoutIdRef.current = null;
62
+ setIsInterviewComplete(
63
+ eventSourceRef.current && eventSourceRef.current.readyState === EventSource.CLOSED
64
+ );
65
+ return;
66
+ }
67
+
68
+ const nextMessage = messageQueue.current.shift();
69
+
70
+ setMessages((prev) => [...prev, nextMessage]);
71
+
72
+ if (nextMessage.audio && isAudioEnabledRef.current) {
73
+ if (currentPlayingAudio.current) {
74
+ currentPlayingAudio.current.pause();
75
+ currentPlayingAudio.current.src = '';
76
+ }
77
+ const audio = new Audio(nextMessage.audio);
78
+ currentPlayingAudio.current = audio;
79
+
80
+ audio.onended = () => {
81
+ currentPlayingAudio.current = null;
82
+ processQueue();
83
+ };
84
+ audio.onerror = (e) => {
85
+ console.error("Audio playback error:", e);
86
+ currentPlayingAudio.current = null;
87
+ processQueue();
88
+ };
89
+ audio.play().catch(e => {
90
+ console.error("Error playing audio automatically:", e);
91
+ currentPlayingAudio.current = null;
92
+ processQueue();
93
+ });
94
+ } else {
95
+ // For non-audio, schedule the next processing call with a fixed delay
96
+ // to simulate reading time. This will call processQueue again, which will
97
+ // handle an empty queue and stop the chain if needed.
98
+ timeoutIdRef.current = setTimeout(processQueue, waitTimeRef.current);
99
+ }
100
+ }, [setMessages, setIsInterviewComplete]);
101
+
102
+ useEffect(() => {
103
+ if (!selectedPatient || !selectedCondition) return;
104
+
105
+ setMessages([]);
106
+ setIsInterviewComplete(false);
107
+ messageQueue.current = [];
108
+ if (currentPlayingAudio.current) {
109
+ currentPlayingAudio.current.pause();
110
+ currentPlayingAudio.current = null;
111
+ }
112
+ // Prepend base URL if running on localhost:3000
113
+ const baseURL =
114
+ window.location.origin === "http://localhost:3000"
115
+ ? "http://localhost:7860"
116
+ : "";
117
+ const url = `${baseURL}/api/stream_conversation?patient=${encodeURIComponent(
118
+ selectedPatient.name
119
+ )}&condition=${encodeURIComponent(selectedCondition)}`;
120
+ const eventSource = new EventSource(url);
121
+ eventSourceRef.current = eventSource;
122
+
123
+ eventSource.onmessage = (event) => {
124
+ try {
125
+ const data = JSON.parse(event.data);
126
+
127
+ // Check if the parsed object is our special 'end' signal
128
+ if (data && data.event === 'end') {
129
+ console.log("Server signaled end of stream. Closing connection.");
130
+ eventSource.close();
131
+ processQueue();
132
+ return;
133
+ }
134
+ messageQueue.current.push(data);
135
+ // Always call processQueue after pushing a message, unless audio or timeout is active
136
+ if (!currentPlayingAudio.current && !timeoutIdRef.current) {
137
+ processQueue();
138
+ }
139
+ } catch (error) {
140
+ console.warn("Could not parse message data. Data received:", event.data, "Error:", error);
141
+ }
142
+ };
143
+
144
+ eventSource.onerror = (err) => {
145
+ console.error("EventSource failed:", err);
146
+ eventSource.close();
147
+ };
148
+
149
+
150
+ return () => {
151
+ if (eventSourceRef.current) {
152
+ eventSourceRef.current.close();
153
+ eventSourceRef.current = null;
154
+ }
155
+ if (timeoutIdRef.current) {
156
+ clearTimeout(timeoutIdRef.current);
157
+ timeoutIdRef.current = null;
158
+ }
159
+ // Ensure any playing audio is stopped when component unmounts or dependencies change
160
+ if (currentPlayingAudio.current) {
161
+ currentPlayingAudio.current.pause();
162
+ currentPlayingAudio.current = null;
163
+ }
164
+ };
165
+ }, [selectedPatient, selectedCondition, processQueue]);
166
+
167
+ useEffect(() => {
168
+ processQueue();
169
+ }, [waitTime, processQueue]);
170
+
171
+ useEffect(() => {
172
+ // Prevent body scroll when Interview is shown
173
+ document.body.style.overflowY = "clip";
174
+ return () => {
175
+ document.body.style.overflowY = "unset";
176
+ };
177
+ }, []);
178
+
179
+ useEffect(() => {
180
+ if (chatContainerRef.current) {
181
+ const container = chatContainerRef.current;
182
+ const lastMessage = messages[messages.length - 1];
183
+ if (lastMessage && lastMessage.speaker === "report") {
184
+ return;
185
+ }
186
+
187
+ const isNearBottom =
188
+ container.scrollHeight - container.scrollTop - container.clientHeight <
189
+ container.clientHeight;
190
+ if (isNearBottom && messages.length > 0) {
191
+ lastMessageRef.current.scrollIntoView({
192
+ behavior: "smooth",
193
+ block: "end",
194
+ });
195
+ }
196
+ }
197
+ }, [messages]);
198
+
199
+ // Update report on new messages
200
+ useEffect(() => {
201
+ const reportMessages = messages.filter((msg) => msg.speaker === "report");
202
+ if (reportMessages.length > 0) {
203
+ const latestReportMessageText =
204
+ reportMessages[reportMessages.length - 1].text;
205
+ const newReport = marked(latestReportMessageText.trim());
206
+ if (newReport !== currentReport) {
207
+ setPrevReport(currentReport);
208
+ setCurrentReport(newReport);
209
+ }
210
+ }
211
+ }, [messages, currentReport]);
212
+
213
+ // Updated diff function to tokenize HTML and use nested diffWords for text changes
214
+ const getDiffReport = () => {
215
+ // Tokenize HTML into tags and text parts
216
+ const tokenizeHTML = (html) => html.match(/(<[^>]+>|[^<]+)/g) || [];
217
+ const tokensPrev = tokenizeHTML(prevReport);
218
+ const tokensCurrent = tokenizeHTML(currentReport);
219
+ const diffParts = diffArrays(tokensPrev, tokensCurrent);
220
+
221
+ let result = "";
222
+ for (let i = 0; i < diffParts.length; i++) {
223
+ // If a removed part is immediately followed by an added part,
224
+ // and both are plain text (not an HTML tag), apply inner diffWords.
225
+ if (
226
+ diffParts[i].removed &&
227
+ i + 1 < diffParts.length &&
228
+ diffParts[i + 1].added
229
+ ) {
230
+ const removedText = diffParts[i].value.join("");
231
+ const addedText = diffParts[i + 1].value.join("");
232
+ // Check if both parts are not HTML tags
233
+ if (
234
+ (!/^<[^>]+>$/.test(removedText) && !/^<[^>]+>$/.test(addedText))
235
+ ) {
236
+ const innerDiff = diffWords(removedText, addedText);
237
+ const innerResult = innerDiff
238
+ .map((part) => {
239
+ if (part.added) {
240
+ return `<span class="add">${part.value}</span>`;
241
+ } else if (part.removed) {
242
+ return `<span class="remove">${part.value}</span>`;
243
+ }
244
+ return part.value;
245
+ })
246
+ .join("");
247
+ result += innerResult;
248
+ i++;
249
+ continue;
250
+ }
251
+ }
252
+ if (diffParts[i].added) {
253
+ result += `<span class="add">${diffParts[i].value.join("")}</span>`;
254
+ } else if (diffParts[i].removed) {
255
+ result += `<span class="remove">${diffParts[i].value.join("")}</span>`;
256
+ } else {
257
+ result += diffParts[i].value.join("");
258
+ }
259
+ }
260
+ return result;
261
+ };
262
+
263
+ // Fetch evaluation when showEvaluation is triggered
264
+ useEffect(() => {
265
+ if (!showEvaluation) return;
266
+ setIsFetchingEvaluation(true);
267
+ setEvaluation('');
268
+ // Get latest report
269
+ const reportMessages = messages.filter((msg) => msg.speaker === "report");
270
+ const report =
271
+ reportMessages.length > 0
272
+ ? marked(reportMessages[reportMessages.length - 1].text.trim())
273
+ : "<p>No report available.</p>";
274
+ // Prepend base URL if running on localhost:3000
275
+ const baseURL = window.location.origin === "http://localhost:3000" ? "http://localhost:7860" : "";
276
+ fetch(`${baseURL}/api/evaluate_report`, {
277
+ method: 'POST',
278
+ headers: { 'Content-Type': 'application/json' },
279
+ body: JSON.stringify({
280
+ report,
281
+ condition: selectedCondition
282
+ })
283
+ })
284
+ .then(response => response.json())
285
+ .then(data => {
286
+ setEvaluation(data.evaluation.replace('```html\n','').replace('\n```',''));
287
+ setIsFetchingEvaluation(false);
288
+ })
289
+ .catch(error => {
290
+ setEvaluation('Error fetching evaluation.');
291
+ setIsFetchingEvaluation(false);
292
+ });
293
+ }, [showEvaluation, messages, selectedCondition]);
294
+
295
+ // Scroll report-content to bottom when evaluate button appears
296
+ useEffect(() => {
297
+ if (isInterviewComplete && reportContentRef.current) {
298
+ reportContentRef.current.scrollTop = reportContentRef.current.scrollHeight;
299
+ }
300
+ }, [isInterviewComplete]);
301
+
302
+ const handleToggleWaitTime = () => {
303
+ setWaitTime((prev) => (prev === 1000 ? 3000 : 1000));
304
+ };
305
+
306
+ const handleToggleAudio = () => {
307
+ setIsAudioEnabled(prev => {
308
+ const isNowEnabled = !prev;
309
+ // If we are disabling audio and something is playing, stop it and continue the queue.
310
+ if (!isNowEnabled && currentPlayingAudio.current) {
311
+ currentPlayingAudio.current.pause();
312
+ currentPlayingAudio.current.src = '';
313
+ currentPlayingAudio.current = null;
314
+ }
315
+ if (!isNowEnabled) {
316
+ setWaitTime(1000);
317
+ }
318
+ return isNowEnabled;
319
+ });
320
+ };
321
+
322
+ const playAudio = (audioDataUrl) => {
323
+ if (audioDataUrl) {
324
+ const audio = new Audio(audioDataUrl);
325
+ audio.play().catch(e => {
326
+ console.error("Error playing audio:", e);
327
+ });
328
+ }
329
+ };
330
+
331
+ return (
332
+ <div className="page interview-page">
333
+ <div className="headerButtonsContainer">
334
+ <button className="back-button" onClick={onBack}>
335
+ <i className="material-icons back-button-icon">keyboard_arrow_left</i>
336
+ Back
337
+ </button>
338
+ <button className="details-button" onClick={() => setIsDetailsPopupOpen(true)}>
339
+ <i className="material-icons code-block-icon">code</i>&nbsp; Details
340
+ about this Demo
341
+ </button>
342
+ </div>
343
+ <div className="frame">
344
+ <div className="interview-split-container">
345
+ {/* Top: Interview Chat */}
346
+ <div className="interview-left-section">
347
+ {/* Right: Chat */}
348
+ <div className="interview-chat-panel">
349
+ <div className="header2">
350
+ Simulated Interview
351
+ &nbsp;
352
+ <div style={{ position: "relative", display: "inline-block" }}>
353
+ <div className="volume-tooltip">
354
+ {isAudioEnabled ? "Disable audio and speed up" : "Enable audio"}
355
+ </div>
356
+ <i
357
+ className="material-icons toggle-icon"
358
+ style={{
359
+ cursor: "pointer",
360
+ color: isAudioEnabled ? "#1976d2" : "#888",
361
+ }}
362
+ title={`Click to ${
363
+ isAudioEnabled ? "disable" : "enable"
364
+ } audio`}
365
+ onClick={handleToggleAudio}
366
+ >
367
+ {isAudioEnabled ? "volume_up" : "volume_off"}
368
+ </i>
369
+ </div>
370
+ {isAudioEnabled && (<span>audio by Gemini TTS</span>)}
371
+ {!isAudioEnabled && (
372
+ <i
373
+ className="material-icons toggle-icon"
374
+ style={{
375
+ cursor: "pointer",
376
+ color: waitTime === 1000 ? "#1976d2" : "#888",
377
+ }}
378
+ title={`Click to ${
379
+ waitTime === 1000 ? "slow down" : "speed up"
380
+ } the interview`}
381
+ onClick={handleToggleWaitTime}
382
+ >
383
+ speed
384
+ </i>)}
385
+
386
+ </div>
387
+ <div className="chat-container" ref={chatContainerRef}>
388
+ {messages.length === 0 ? (
389
+ <div className="chat-waiting-indicator">
390
+ Waiting for the interview to start...
391
+ </div>
392
+ ) : (
393
+ messages
394
+ .filter((msg) => msg.speaker !== "report")
395
+ .map((msg, idx, filteredMessages) => (
396
+ <div
397
+ ref={idx === filteredMessages.length - 1 ? lastMessageRef : null}
398
+ className={`chat-message-wrapper ${msg.speaker}${idx === filteredMessages.length - 1 ? " fade-in" : ""}${msg.audio ? " has-audio" : ""}`}
399
+ key={idx}
400
+ >
401
+ {msg.speaker.includes("interviewer") && (
402
+ <img
403
+ className="chat-avatar"
404
+ src="assets/ai_headshot.svg"
405
+ alt="Interviewer"
406
+ />
407
+ )}
408
+ <div className={`chat-bubble ${msg.audio ? "with-audio" : ""}`}>
409
+ {msg.speaker.includes("thinking") && (
410
+ <div className="thinking-header">Thinking...</div>
411
+ )}
412
+ {msg.text}
413
+ </div>
414
+ {msg.speaker === "patient" && (
415
+ <img
416
+ className="chat-avatar"
417
+ src={selectedPatient.headshot}
418
+ alt={selectedPatient.name}
419
+ />
420
+ )}
421
+ </div>
422
+ ))
423
+ )}
424
+ </div>
425
+ </div>
426
+ </div>
427
+ {/* Right: Report Section */}
428
+ <div className="interview-right-section">
429
+ <div className="header2">Generated Report</div>
430
+ <div className="report-content" ref={reportContentRef}>
431
+ {/* Updated report rendering to show diff if available */}
432
+ <div
433
+ dangerouslySetInnerHTML={{
434
+ __html: prevReport ? getDiffReport() : currentReport,
435
+ }}
436
+ />
437
+ {isInterviewComplete && (
438
+ <button
439
+ className="evaluate-button"
440
+ onClick={() => setShowEvaluationInfoPopup(true)}
441
+ disabled={showEvaluation || showEvaluationInfoPopup}
442
+ ><i className="material-icons back-button-icon">keyboard_arrow_down</i>
443
+ View Report Evaluation
444
+ </button>
445
+ )}
446
+ <div className="evaluation-text">
447
+ {showEvaluation && (
448
+ isFetchingEvaluation
449
+ ? <div>Please wait...</div>
450
+ : parse(evaluation)
451
+ )}
452
+ </div>
453
+ </div>
454
+ <div className="disclaimer-container">
455
+ <i className="material-icons warning-icon">warning</i>
456
+ <div className="disclaimer-text">
457
+ This demonstration is for illustrative purposes of MedGemma’s baseline capabilities only. It does not represent a finished or approved product, is not intended to diagnose or suggest treatment of any disease or condition, and should not be used for medical advice.
458
+ </div>
459
+ </div>
460
+ </div>
461
+ </div>
462
+ </div>
463
+ {showEvaluationInfoPopup && (
464
+ <div className="popup-overlay">
465
+ <div className="popup-content">
466
+ <h2>About the Evaluation</h2>
467
+ <p>
468
+ Now we will ask MedGemma to evaluate its own performance at
469
+ generating this report. We will provide it with all the
470
+ information about {selectedPatient.name}, including their actual
471
+ diagnosis and aspects of condition history not included previously.
472
+ Using this new information, MedGemma will
473
+ highlight key facts it correctly included and identify other
474
+ information that would have been beneficial to add.
475
+ </p>
476
+ <p>
477
+ The purpose of this step is to provide non-medical users with a
478
+ sense of how well MedGemma did at this task. While the evaluation
479
+ is completed by MedGemma, the examples in this demo have also been
480
+ reviewed by clinicians for accuracy. Although MedGemma's evaluation
481
+ does not represent a consensus based standard,
482
+ this illustration simply shows an example of one approach developers could adopt
483
+ to evaluate quality and completeness.
484
+ </p>
485
+ <button className="popup-button" onClick={() => {
486
+ setShowEvaluationInfoPopup(false);
487
+ setShowEvaluation(true);
488
+ }}>Continue</button>
489
+ </div>
490
+ </div>)}
491
+ <DetailsPopup
492
+ isOpen={isDetailsPopupOpen}
493
+ onClose={() => setIsDetailsPopupOpen(false)}
494
+ />
495
+ </div>
496
+ );
497
+ };
498
+
499
+ export default Interview;
frontend/src/components/Landing/Landing.css ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ .sushruta-landing {
18
+ min-height: 100vh;
19
+ background: linear-gradient(135deg, var(--sushruta-bg-primary) 0%, var(--sushruta-bg-secondary) 100%);
20
+ color: var(--sushruta-text-primary);
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ padding: 40px 20px;
26
+ position: relative;
27
+ overflow: hidden;
28
+ }
29
+
30
+ /* Background Glowing Orbs */
31
+ .orb {
32
+ position: absolute;
33
+ width: 400px;
34
+ height: 400px;
35
+ border-radius: 50%;
36
+ filter: blur(120px);
37
+ opacity: 0.15;
38
+ pointer-events: none;
39
+ z-index: 0;
40
+ }
41
+
42
+ .orb-blue {
43
+ top: 10%;
44
+ left: 10%;
45
+ background: var(--sushruta-accent-blue);
46
+ animation: floatOrb 12s infinite alternate ease-in-out;
47
+ }
48
+
49
+ .orb-purple {
50
+ bottom: 15%;
51
+ right: 10%;
52
+ background: var(--sushruta-accent-purple);
53
+ animation: floatOrb 16s infinite alternate-reverse ease-in-out;
54
+ }
55
+
56
+ @keyframes floatOrb {
57
+ 0% {
58
+ transform: translate(0, 0) scale(1);
59
+ }
60
+ 100% {
61
+ transform: translate(50px, 30px) scale(1.1);
62
+ }
63
+ }
64
+
65
+ /* Header */
66
+ .sushruta-header {
67
+ text-align: center;
68
+ margin-top: 40px;
69
+ margin-bottom: 20px;
70
+ z-index: 1;
71
+ animation: fadeInDown 0.8s ease-out;
72
+ }
73
+
74
+ .sushruta-title {
75
+ font-family: 'Google Sans', sans-serif;
76
+ font-size: 54px;
77
+ font-weight: 700;
78
+ margin: 0;
79
+ letter-spacing: -1.5px;
80
+ background: linear-gradient(90deg, #ffffff 30%, var(--sushruta-accent-teal) 100%);
81
+ -webkit-background-clip: text;
82
+ -webkit-text-fill-color: transparent;
83
+ position: relative;
84
+ }
85
+
86
+ .sushruta-tagline {
87
+ font-size: 18px;
88
+ color: var(--sushruta-text-secondary);
89
+ margin-top: 10px;
90
+ font-weight: 400;
91
+ letter-spacing: 0.5px;
92
+ }
93
+
94
+ /* Main Layout */
95
+ .sushruta-main {
96
+ width: 100%;
97
+ max-width: 1100px;
98
+ z-index: 1;
99
+ display: flex;
100
+ justify-content: center;
101
+ align-items: center;
102
+ flex-grow: 1;
103
+ margin: 40px 0;
104
+ }
105
+
106
+ .module-grid {
107
+ display: grid;
108
+ grid-template-columns: repeat(2, 1fr);
109
+ gap: 40px;
110
+ width: 100%;
111
+ }
112
+
113
+ @media (max-width: 768px) {
114
+ .module-grid {
115
+ grid-template-columns: 1fr;
116
+ gap: 30px;
117
+ }
118
+ .sushruta-title {
119
+ font-size: 40px;
120
+ }
121
+ }
122
+
123
+ /* Module Cards with Glassmorphism */
124
+ .module-card {
125
+ position: relative;
126
+ background: var(--sushruta-bg-card);
127
+ backdrop-filter: blur(16px);
128
+ -webkit-backdrop-filter: blur(16px);
129
+ border-radius: 24px;
130
+ padding: 3px; /* For the gradient border */
131
+ cursor: pointer;
132
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
133
+ overflow: hidden;
134
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
135
+ animation: fadeInUp 0.8s ease-out;
136
+ }
137
+
138
+ .card-inner {
139
+ background: #111124;
140
+ border-radius: 21px;
141
+ padding: 40px 30px;
142
+ height: 100%;
143
+ display: flex;
144
+ flex-direction: column;
145
+ justify-content: space-between;
146
+ align-items: flex-start;
147
+ transition: background 0.3s ease;
148
+ }
149
+
150
+ .card-icon {
151
+ font-size: 48px;
152
+ margin-bottom: 24px;
153
+ }
154
+
155
+ .card-title {
156
+ font-family: 'Google Sans', sans-serif;
157
+ font-size: 26px;
158
+ font-weight: 600;
159
+ color: #ffffff;
160
+ margin: 0 0 16px 0;
161
+ }
162
+
163
+ .card-description {
164
+ font-size: 16px;
165
+ line-height: 1.6;
166
+ color: var(--sushruta-text-secondary);
167
+ margin: 0 0 32px 0;
168
+ flex-grow: 1;
169
+ }
170
+
171
+ .card-action {
172
+ font-family: 'Google Sans', sans-serif;
173
+ font-size: 16px;
174
+ font-weight: 600;
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 8px;
178
+ transition: gap 0.3s ease;
179
+ }
180
+
181
+ .card-action .arrow {
182
+ transition: transform 0.3s ease;
183
+ }
184
+
185
+ /* Card hover glow & gradient borders */
186
+ .card-previsit .card-border-gradient {
187
+ position: absolute;
188
+ top: 0;
189
+ left: 0;
190
+ right: 0;
191
+ bottom: 0;
192
+ background: linear-gradient(135deg, var(--sushruta-accent-blue) 0%, var(--sushruta-accent-purple) 100%);
193
+ z-index: -1;
194
+ opacity: 0.3;
195
+ transition: opacity 0.4s ease;
196
+ }
197
+
198
+ .card-radiology .card-border-gradient {
199
+ position: absolute;
200
+ top: 0;
201
+ left: 0;
202
+ right: 0;
203
+ bottom: 0;
204
+ background: linear-gradient(135deg, var(--sushruta-accent-teal) 0%, var(--sushruta-accent-blue) 100%);
205
+ z-index: -1;
206
+ opacity: 0.3;
207
+ transition: opacity 0.4s ease;
208
+ }
209
+
210
+ /* Hover effects */
211
+ .module-card:hover {
212
+ transform: translateY(-8px) scale(1.01);
213
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
214
+ }
215
+
216
+ .card-previsit:hover {
217
+ box-shadow: 0 0 30px rgba(79, 143, 255, 0.2);
218
+ }
219
+
220
+ .card-radiology:hover {
221
+ box-shadow: 0 0 30px rgba(0, 212, 170, 0.2);
222
+ }
223
+
224
+ .module-card:hover .card-border-gradient {
225
+ opacity: 1;
226
+ }
227
+
228
+ .module-card:hover .card-inner {
229
+ background: #14142b;
230
+ }
231
+
232
+ .module-card:hover .card-action {
233
+ gap: 12px;
234
+ }
235
+
236
+ .module-card:hover .card-action .arrow {
237
+ transform: translateX(4px);
238
+ }
239
+
240
+ .card-previsit .card-action {
241
+ color: var(--sushruta-accent-blue);
242
+ }
243
+
244
+ .card-radiology .card-action {
245
+ color: var(--sushruta-accent-teal);
246
+ }
247
+
248
+ /* Footer & Disclaimer */
249
+ .sushruta-footer {
250
+ width: 100%;
251
+ max-width: 900px;
252
+ text-align: center;
253
+ z-index: 1;
254
+ margin-top: 40px;
255
+ animation: fadeInUp 1s ease-out;
256
+ }
257
+
258
+ .disclaimer-box {
259
+ background: rgba(255, 255, 255, 0.03);
260
+ border: 1px solid rgba(255, 255, 255, 0.08);
261
+ border-radius: 12px;
262
+ padding: 16px 24px;
263
+ font-size: 13px;
264
+ line-height: 1.5;
265
+ color: #8a8a9a;
266
+ text-align: justify;
267
+ margin-bottom: 24px;
268
+ }
269
+
270
+ .footer-credits {
271
+ font-size: 13px;
272
+ color: #6a6a7a;
273
+ }
274
+
275
+ /* Entrance Animations */
276
+ @keyframes fadeInDown {
277
+ from {
278
+ opacity: 0;
279
+ transform: translateY(-20px);
280
+ }
281
+ to {
282
+ opacity: 1;
283
+ transform: translateY(0);
284
+ }
285
+ }
286
+
287
+ @keyframes fadeInUp {
288
+ from {
289
+ opacity: 0;
290
+ transform: translateY(20px);
291
+ }
292
+ to {
293
+ opacity: 1;
294
+ transform: translateY(0);
295
+ }
296
+ }
frontend/src/components/Landing/Landing.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import './Landing.css';
19
+
20
+ const Landing = ({ onNavigate }) => {
21
+ return (
22
+ <div className="sushruta-landing">
23
+ {/* Background glowing orbs */}
24
+ <div className="orb orb-blue"></div>
25
+ <div className="orb orb-purple"></div>
26
+
27
+ <header className="sushruta-header">
28
+ <h1 className="sushruta-title">
29
+ Sushruta
30
+ <span className="title-glow"></span>
31
+ </h1>
32
+ <p className="sushruta-tagline">Patient 360 — Powered by MedGemma</p>
33
+ </header>
34
+
35
+ <main className="sushruta-main">
36
+ <div className="module-grid">
37
+ {/* Card 1: Pre-Visit Intake */}
38
+ <div
39
+ className="module-card card-previsit"
40
+ onClick={() => onNavigate('previsit')}
41
+ id="card-previsit-intake"
42
+ >
43
+ <div className="card-border-gradient"></div>
44
+ <div className="card-inner">
45
+ <div className="card-icon">📋</div>
46
+ <h2 className="card-title">Pre-Visit Intake</h2>
47
+ <p className="card-description">
48
+ AI-powered patient interview simulation. Watch MedGemma conduct a pre-visit intake, gather clinical history, and automatically generate an intake report.
49
+ </p>
50
+ <div className="card-action">
51
+ <span>Launch Intake</span>
52
+ <span className="arrow">→</span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ {/* Card 2: Radiology Explainer */}
58
+ <div
59
+ className="module-card card-radiology"
60
+ onClick={() => onNavigate('radiology')}
61
+ id="card-radiology-explainer"
62
+ >
63
+ <div className="card-border-gradient"></div>
64
+ <div className="card-inner">
65
+ <div className="card-icon">🩺</div>
66
+ <h2 className="card-title">Radiology Explainer</h2>
67
+ <p className="card-description">
68
+ Explore radiology images and clinical reports. Select any sentence to receive an AI-powered plain-language explanation with localized image-pointing guidance.
69
+ </p>
70
+ <div className="card-action">
71
+ <span>Launch Explainer</span>
72
+ <span className="arrow">→</span>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </main>
78
+
79
+ <footer className="sushruta-footer">
80
+ <div className="disclaimer-box">
81
+ <strong>Disclaimer:</strong> This demonstration is for illustrative and developer exploration purposes only. It does not represent a finished, certified, or approved medical product. It is not intended to be used for diagnostic purposes or real clinical decision-making.
82
+ </div>
83
+ <p className="footer-credits">
84
+ Built on Hugging Face Spaces using MedGemma and Gemma-3.
85
+ </p>
86
+ </footer>
87
+ </div>
88
+ );
89
+ };
90
+
91
+ export default Landing;
frontend/src/components/PatientBuilder/PatientBuilder.css ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ #root {
18
+ display: flex;
19
+ justify-content: center;
20
+ }
21
+
22
+ .header2 {
23
+ font-size: 24px;
24
+ font-weight: bold;
25
+ margin-bottom: 10px;
26
+ }
27
+
28
+ .lighttext {
29
+ font-size: 15px;
30
+ }
31
+
32
+
33
+
34
+ .patient-builder-container {
35
+ font-family: Arial, sans-serif;
36
+ display: flex;
37
+ flex-direction: column;
38
+ position: relative;
39
+ gap: 20px;
40
+ width: min-content;
41
+ height: min-content;
42
+ }
43
+
44
+ .patient-list {
45
+ display: flex;
46
+ gap: 20px;
47
+ }
48
+
49
+ .patient-list {
50
+ justify-content: space-between;
51
+ }
52
+
53
+ .selection-section {
54
+ margin-bottom: 30px;
55
+ flex-direction: column;
56
+ width: 958px;
57
+ }
58
+
59
+ .condition-list {
60
+ align-items: stretch;
61
+ flex-direction: column;
62
+ gap: 10px;
63
+ margin-top: 10px;
64
+ display: grid;
65
+ grid-template-columns: 1fr 1fr;
66
+ }
67
+
68
+ .condition-card {
69
+ display: grid;
70
+ grid-template-columns: 100px 1fr;
71
+ align-items: center;
72
+ padding: 0 30px;
73
+ }
74
+
75
+ .condition-card {
76
+ background: #fff;
77
+ border: 2px solid #ddd;
78
+ border-radius: 10px;
79
+ padding: 10px;
80
+ cursor: pointer;
81
+ transition: transform 0.2s ease, border-color 0.2s ease;
82
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
83
+ }
84
+
85
+ .patient-video-container {
86
+ position: relative;
87
+ cursor: pointer;
88
+ width: 300px;
89
+ height: 300px;
90
+ overflow: hidden;
91
+ border: 4px solid transparent;
92
+ border-radius: 12px;
93
+ transition: border-color 0.2s ease;
94
+ box-sizing: border-box;
95
+ }
96
+
97
+ .patient-video, .patient-img {
98
+ position: absolute;
99
+ top: 0;
100
+ left: 0;
101
+ width: 100%;
102
+ object-fit: cover;
103
+ border-radius: 8px;
104
+ transition: opacity 0.4s ease-in-out;
105
+ }
106
+
107
+ .ehr-label {
108
+ position: absolute;
109
+ bottom: 15px;
110
+ right: 10px;
111
+ border-radius: 4px;
112
+ border: 1px solid #C8B3FD;
113
+ background: #E8DEF8;
114
+ padding: 0 5px;
115
+ }
116
+
117
+ .patient-video-container:hover {
118
+ border-color: #aaa;
119
+ }
120
+
121
+ .condition-card:hover, .ehr-label:hover {
122
+ transform: scale(1.05);
123
+ border-color: #aaa;
124
+ }
125
+ .patient-video-container.selected {
126
+ border-color: #D0BCFF;
127
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
128
+ }
129
+
130
+ .condition-card.selected {
131
+ border: 4px solid #D0BCFF;
132
+ box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
133
+ }
134
+
135
+ .go-button {
136
+ background: #0078D7;
137
+ color: #fff;
138
+ border: none;
139
+ padding: 10px 20px;
140
+ border-radius: 5px;
141
+ cursor: pointer;
142
+ font-size: 16px;
143
+ transition: background 0.3s ease;
144
+ }
145
+ .go-button:disabled {
146
+ background: #aaa;
147
+ cursor: not-allowed;
148
+ }
149
+ .go-button:hover:not(:disabled) {
150
+ background: #005fa3;
151
+ }
152
+ .patient-info .category-label {
153
+ font-size: 12px;
154
+ font-weight: bold;
155
+ }
156
+ .patient-info .category-value {
157
+ font-size: 16px;
158
+ font-weight: normal;
159
+ }
160
+
161
+ .patient-info {
162
+ display: flex;
163
+ flex-direction: column;
164
+ justify-content: center;
165
+ }
166
+
167
+ .condition-card.disabled {
168
+ pointer-events: none;
169
+ opacity: 0.3;
170
+ transition: opacity 0.2s ease-in-out 0.1s;
171
+ }
172
+
173
+ .patient-info-right {
174
+ display: flex;
175
+ flex-direction: column;
176
+ justify-content: center;
177
+ gap: 10px;
178
+ width: min-content;
179
+ }
180
+
181
+ .patient-details {
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 10px;
185
+ margin-top: 20px;
186
+ align-items: center;
187
+ text-align: center;
188
+ }
189
+
190
+ .json-popup-content {
191
+ max-width: 80vw;
192
+ max-height: 80vh;
193
+ display: flex;
194
+ flex-direction: column;
195
+ width: 800px;
196
+ }
197
+
198
+ .json-viewer-container {
199
+ flex-grow: 1;
200
+ overflow: auto;
201
+ border-radius: 8px;
202
+ padding: 1rem;
203
+ margin-bottom: 1.5rem;
204
+ font-family: monospace;
205
+ }
frontend/src/components/PatientBuilder/PatientBuilder.js ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React, { useState, useEffect } from "react";
18
+ import "./PatientBuilder.css";
19
+ import { JsonViewer } from "@textea/json-viewer"; // updated import
20
+ import DetailsPopup from "../DetailsPopup/DetailsPopup";
21
+
22
+ // Global caching function to load patients & conditions once
23
+ let cachedPatientsAndConditions = null;
24
+ function getPatientsAndConditions() {
25
+ if (cachedPatientsAndConditions)
26
+ return Promise.resolve(cachedPatientsAndConditions);
27
+ return fetch("/assets/patients_and_conditions.json")
28
+ .then((response) => response.json())
29
+ .then((data) => {
30
+ cachedPatientsAndConditions = data;
31
+ return data;
32
+ });
33
+ }
34
+
35
+ const PatientBuilder = ({
36
+ selectedPatient,
37
+ selectedCondition,
38
+ setSelectedPatient,
39
+ setSelectedCondition,
40
+ onNext,
41
+ onBack,
42
+ }) => {
43
+ const [patients, setPatients] = useState([]);
44
+ const [conditions, setConditions] = useState([]);
45
+ const [hoveredPatient, setHoveredPatient] = useState(null);
46
+ const [isVideoLoading, setIsVideoLoading] = useState(false);
47
+
48
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
49
+ const [popupJson, setPopupJson] = useState(null);
50
+ const [isDetailsPopupOpen, setIsDetailsPopupOpen] = useState(false);
51
+
52
+
53
+ useEffect(() => {
54
+ getPatientsAndConditions()
55
+ .then((data) => {
56
+ setPatients(data.patients);
57
+ setConditions(data.conditions);
58
+ })
59
+ .catch((error) =>
60
+ console.error("Error fetching patients and conditions:", error)
61
+ );
62
+ }, []);
63
+
64
+ useEffect(() => {
65
+ if (
66
+ (selectedPatient &&
67
+ selectedPatient.existing_condition !== "depression" &&
68
+ selectedCondition === "Serotonin Syndrome")
69
+ || (
70
+ selectedCondition === "Migraine" &&
71
+ selectedPatient &&
72
+ selectedPatient.existing_condition === "Diabetes"
73
+ )
74
+ ) {
75
+ setSelectedCondition(null);
76
+ }
77
+ }, [selectedPatient]);
78
+
79
+ // When a new patient is selected, set the video to a loading state
80
+ // to ensure the placeholder image is shown.
81
+ useEffect(() => {
82
+ if (selectedPatient) {
83
+ setIsVideoLoading(true);
84
+ }
85
+ }, [selectedPatient]);
86
+
87
+ const handleGo = () => {
88
+ if (selectedPatient && selectedCondition) {
89
+ onNext();
90
+ }
91
+ };
92
+
93
+ const openPopup = (patient) => {
94
+ if (patient && patient.fhirFile) {
95
+ fetch(patient.fhirFile)
96
+ .then((response) => response.json())
97
+ .then((json) => {
98
+ setPopupJson(json);
99
+ setIsPopupOpen(true);
100
+ })
101
+ .catch((error) => console.error("Error fetching FHIR JSON:", error));
102
+ }
103
+ };
104
+
105
+ const closePopup = () => {
106
+ setIsPopupOpen(false);
107
+ setPopupJson(null);
108
+ };
109
+
110
+ return (
111
+ <div className="patient-builder-container">
112
+ <div className="headerButtonsContainer">
113
+ <button className="back-button" onClick={onBack}>
114
+ <i className="material-icons back-button-icon">keyboard_arrow_left</i>
115
+ Back
116
+ </button>
117
+ <button className="details-button" onClick={() => setIsDetailsPopupOpen(true)}>
118
+ <i className="material-icons code-block-icon">code</i>&nbsp;
119
+ Details about this Demo
120
+ </button>
121
+ </div>
122
+ <div className="frame">
123
+ <div className="selection-section">
124
+ <div className="header2">Select a Patient</div>
125
+ <div className="patient-list">
126
+ {patients.map((patient) => {
127
+ const isSelected = selectedPatient && selectedPatient.id === patient.id;
128
+ return (
129
+ <div
130
+ key={patient.id}
131
+ className="patient-card"
132
+ >
133
+ <div
134
+ className={`patient-video-container ${isSelected ? "selected" : ""}`}
135
+ onClick={() => setSelectedPatient(patient)}
136
+ >
137
+ <img
138
+ src={patient.img}
139
+ className="patient-img"
140
+ alt={patient.name}
141
+ draggable="false"
142
+ onDragStart={(e) => e.preventDefault()}
143
+ style={{ opacity: isSelected && !isVideoLoading ? 0 : 1 }}
144
+ />
145
+ {isSelected && (
146
+ <video
147
+ key={patient.id}
148
+ src={patient.video}
149
+ className="patient-video"
150
+ autoPlay
151
+ muted
152
+ loop
153
+ onCanPlay={() => setIsVideoLoading(false)}
154
+ style={{ opacity: isVideoLoading ? 0 : 1 }}
155
+ />
156
+ )}
157
+ <div className="ehr-label" onClick={(e) => { e.stopPropagation(); openPopup(patient); }}>
158
+ Synthetic Health Record (FHIR)
159
+ </div>
160
+ </div>
161
+ <div className="patient-info">
162
+ <div className="category-value">
163
+ {patient.name}, {patient.age} years old, {patient.gender}
164
+ </div>
165
+ <div className="category-value">
166
+ Existing condition: {patient.existing_condition}
167
+ </div>
168
+ </div>
169
+ </div>
170
+ );
171
+ })}
172
+ </div>
173
+ </div>
174
+ <div className="selection-section">
175
+ <div className="header2">Explore a Condition</div>
176
+ <div className="lighttext">
177
+ In this demonstration, a persona, simulated using Gemini 2.5 Flash, will interact with an AI agent, built with MedGemma.
178
+ Neither the simulated persona nor the AI agent have been provided the diagnosis for the current condition (selected below).
179
+ The AI agent facilitates structured information-gathering, designed to usefully collect and summarize the patient's symptoms.
180
+ For the purposes of this demonstration, the AI agent also has access to elements of the patient's health record (provided as FHIR resources).
181
+ </div>
182
+ <div className="condition-list">
183
+ {conditions.map((cond) => {
184
+ const isDisabled =
185
+ (cond.name === "Serotonin Syndrome" &&
186
+ selectedPatient &&
187
+ selectedPatient.existing_condition !== "Depression") || (
188
+ cond.name === "Migraine" &&
189
+ selectedPatient &&
190
+ selectedPatient.existing_condition === "Diabetes");
191
+ return (
192
+ <div
193
+ key={cond.name}
194
+ className={`condition-card lighttext ${
195
+ selectedCondition === cond.name ? "selected" : ""
196
+ } ${isDisabled ? "disabled" : ""}`}
197
+ onClick={
198
+ !isDisabled
199
+ ? () => setSelectedCondition(cond.name)
200
+ : undefined
201
+ }
202
+ >
203
+ <div><strong>{cond.name}</strong></div>
204
+ <div>{cond.description}</div>
205
+ </div>
206
+ );
207
+ })}
208
+ </div>
209
+ </div>
210
+ <button
211
+ className="info-button"
212
+ onClick={handleGo}
213
+ disabled={!(selectedPatient && selectedCondition)}
214
+ >
215
+ Launch simulation
216
+ </button>
217
+ </div>
218
+ {isPopupOpen && (
219
+ <div className="popup-overlay" onClick={closePopup}>
220
+ <div
221
+ className="popup-content json-popup-content"
222
+ onClick={(e) => e.stopPropagation()}
223
+ >
224
+ <h2>Synthetic Electronic Health Record</h2>
225
+ <span>This is a sample of the patient’s electronic health record, shown in a standard (FHIR) format. This FHIR record, like the patient, was generated solely for the purposes of this demo.</span>
226
+ <div className="json-viewer-container">
227
+ <JsonViewer value={popupJson} theme="monokai" />
228
+ </div>
229
+ <button className="popup-button" onClick={closePopup}>
230
+ Close
231
+ </button>
232
+ </div>
233
+ </div>
234
+ )}
235
+ <DetailsPopup
236
+ isOpen={isDetailsPopupOpen}
237
+ onClose={() => setIsDetailsPopupOpen(false)}
238
+ />
239
+ </div>
240
+ );
241
+ };
242
+
243
+ export default PatientBuilder;
frontend/src/components/PreloadImages.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React, { useEffect, useState } from 'react';
18
+
19
+ const PreloadImages = ({ imageSources, children }) => {
20
+ const [loaded, setLoaded] = useState(false);
21
+
22
+ useEffect(() => {
23
+ let loadedCount = 0;
24
+ imageSources.forEach(src => {
25
+ const img = new Image();
26
+ img.src = src;
27
+ img.onload = () => {
28
+ loadedCount++;
29
+ if (loadedCount === imageSources.length) {
30
+ setLoaded(true);
31
+ }
32
+ };
33
+ });
34
+ }, [imageSources]);
35
+
36
+ if (!loaded) {
37
+ return <div>Loading images...</div>;
38
+ }
39
+ return <>{children}</>;
40
+ };
41
+
42
+ export default PreloadImages;
frontend/src/components/RadiologyExplainer/RadiologyExplainer.css ADDED
@@ -0,0 +1,424 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ .radiology-explainer {
18
+ min-height: 100vh;
19
+ background: var(--sushruta-bg-primary);
20
+ color: var(--sushruta-text-primary);
21
+ display: flex;
22
+ flex-direction: column;
23
+ position: relative;
24
+ overflow-x: hidden;
25
+ }
26
+
27
+ /* Background glows */
28
+ .radiology-orb-left {
29
+ position: absolute;
30
+ top: -10%;
31
+ left: -10%;
32
+ width: 400px;
33
+ height: 400px;
34
+ border-radius: 50%;
35
+ background: var(--sushruta-accent-teal);
36
+ filter: blur(140px);
37
+ opacity: 0.08;
38
+ pointer-events: none;
39
+ z-index: 0;
40
+ }
41
+
42
+ .radiology-orb-right {
43
+ position: absolute;
44
+ bottom: -10%;
45
+ right: -10%;
46
+ width: 450px;
47
+ height: 450px;
48
+ border-radius: 50%;
49
+ background: var(--sushruta-accent-blue);
50
+ filter: blur(140px);
51
+ opacity: 0.08;
52
+ pointer-events: none;
53
+ z-index: 0;
54
+ }
55
+
56
+ /* Navigation */
57
+ .radiology-nav {
58
+ display: flex;
59
+ justify-content: space-between;
60
+ align-items: center;
61
+ padding: 16px 32px;
62
+ background: rgba(10, 10, 26, 0.6);
63
+ backdrop-filter: blur(10px);
64
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
65
+ z-index: 10;
66
+ }
67
+
68
+ .radiology-logo {
69
+ font-family: 'Google Sans', sans-serif;
70
+ font-size: 22px;
71
+ font-weight: 600;
72
+ margin: 0;
73
+ background: linear-gradient(90deg, #ffffff 40%, var(--sushruta-accent-teal) 100%);
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ }
77
+
78
+ /* Nav buttons customized for dark mode */
79
+ .radiology-nav .back-button {
80
+ color: var(--sushruta-text-secondary);
81
+ border: 1px solid rgba(255, 255, 255, 0.15);
82
+ background: transparent;
83
+ transition: all 0.3s ease;
84
+ }
85
+
86
+ .radiology-nav .back-button:hover {
87
+ color: #ffffff;
88
+ border-color: rgba(255, 255, 255, 0.3);
89
+ background: rgba(255, 255, 255, 0.05);
90
+ }
91
+
92
+ .radiology-nav .details-button {
93
+ background: rgba(0, 212, 170, 0.15);
94
+ color: var(--sushruta-accent-teal);
95
+ border: 1px solid rgba(0, 212, 170, 0.3);
96
+ transition: all 0.3s ease;
97
+ }
98
+
99
+ .radiology-nav .details-button:hover {
100
+ background: rgba(0, 212, 170, 0.25);
101
+ box-shadow: 0 0 12px rgba(0, 212, 170, 0.2);
102
+ }
103
+
104
+ /* Main Container Layout */
105
+ .radiology-container {
106
+ display: grid;
107
+ grid-template-columns: 240px 1fr 400px;
108
+ gap: 24px;
109
+ padding: 24px 32px;
110
+ flex-grow: 1;
111
+ z-index: 1;
112
+ max-width: 1600px;
113
+ width: 100%;
114
+ margin: 0 auto;
115
+ min-height: 0; /* Important for grid item scrollability */
116
+ }
117
+
118
+ @media (max-width: 1200px) {
119
+ .radiology-container {
120
+ grid-template-columns: 200px 1fr;
121
+ grid-template-rows: auto;
122
+ }
123
+ .radiology-report-panel {
124
+ grid-column: 1 / -1;
125
+ }
126
+ }
127
+
128
+ @media (max-width: 768px) {
129
+ .radiology-container {
130
+ grid-template-columns: 1fr;
131
+ }
132
+ .radiology-sidebar,
133
+ .radiology-main,
134
+ .radiology-report-panel {
135
+ grid-column: 1 / -1;
136
+ }
137
+ }
138
+
139
+ /* Sidebar: Case Selection */
140
+ .radiology-sidebar {
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: 24px;
144
+ }
145
+
146
+ .sidebar-group h3 {
147
+ font-family: 'Google Sans', sans-serif;
148
+ font-size: 15px;
149
+ font-weight: 600;
150
+ text-transform: uppercase;
151
+ letter-spacing: 0.8px;
152
+ color: var(--sushruta-text-secondary);
153
+ margin: 0 0 12px 0;
154
+ border-left: 3px solid var(--sushruta-accent-teal);
155
+ padding-left: 8px;
156
+ }
157
+
158
+ .case-buttons {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 10px;
162
+ }
163
+
164
+ .case-btn {
165
+ font-family: 'Google Sans Text', sans-serif;
166
+ font-size: 14px;
167
+ font-weight: 500;
168
+ text-align: left;
169
+ padding: 12px 16px;
170
+ background: var(--sushruta-bg-card);
171
+ border: 1px solid rgba(255, 255, 255, 0.08);
172
+ border-radius: 12px;
173
+ color: var(--sushruta-text-primary);
174
+ cursor: pointer;
175
+ transition: all 0.3s ease;
176
+ }
177
+
178
+ .case-btn:hover {
179
+ background: rgba(255, 255, 255, 0.08);
180
+ border-color: rgba(255, 255, 255, 0.15);
181
+ }
182
+
183
+ .case-btn.active {
184
+ background: rgba(79, 143, 255, 0.12);
185
+ border-color: var(--sushruta-accent-blue);
186
+ color: #ffffff;
187
+ box-shadow: 0 0 12px rgba(79, 143, 255, 0.15);
188
+ }
189
+
190
+ .no-cases {
191
+ font-size: 13px;
192
+ color: #6a6a7a;
193
+ font-style: italic;
194
+ }
195
+
196
+ /* Center Panel: Image Viewer */
197
+ .radiology-main {
198
+ min-width: 0;
199
+ }
200
+
201
+ .image-card {
202
+ background: var(--sushruta-bg-card);
203
+ border: 1px solid rgba(255, 255, 255, 0.08);
204
+ border-radius: 20px;
205
+ overflow: hidden;
206
+ height: 100%;
207
+ display: flex;
208
+ flex-direction: column;
209
+ justify-content: space-between;
210
+ }
211
+
212
+ .image-card-header {
213
+ background: rgba(255, 255, 255, 0.03);
214
+ padding: 14px 20px;
215
+ font-family: 'Google Sans', sans-serif;
216
+ font-size: 15px;
217
+ font-weight: 600;
218
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
219
+ text-align: center;
220
+ color: var(--sushruta-text-secondary);
221
+ }
222
+
223
+ .image-viewer-container {
224
+ flex-grow: 1;
225
+ display: flex;
226
+ justify-content: center;
227
+ align-items: center;
228
+ background: #05050f;
229
+ padding: 20px;
230
+ min-height: 380px;
231
+ }
232
+
233
+ .radiology-image {
234
+ max-width: 100%;
235
+ max-height: 520px;
236
+ border-radius: 12px;
237
+ object-fit: contain;
238
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
239
+ }
240
+
241
+ .image-loading {
242
+ font-size: 15px;
243
+ color: var(--sushruta-text-secondary);
244
+ animation: pulse 1.5s infinite;
245
+ }
246
+
247
+ .image-placeholder {
248
+ font-size: 14px;
249
+ color: #555565;
250
+ }
251
+
252
+ .image-footer-note {
253
+ background: rgba(255, 255, 255, 0.02);
254
+ padding: 10px 16px;
255
+ font-size: 12px;
256
+ color: #6a6a7a;
257
+ text-align: center;
258
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
259
+ }
260
+
261
+ /* Right Panel: Report & Explanations */
262
+ .radiology-report-panel {
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 20px;
266
+ min-height: 0;
267
+ }
268
+
269
+ /* Explanation Card */
270
+ .explanation-card {
271
+ background: rgba(79, 143, 255, 0.04);
272
+ border: 1px solid rgba(79, 143, 255, 0.15);
273
+ border-radius: 16px;
274
+ padding: 20px;
275
+ position: relative;
276
+ overflow: hidden;
277
+ }
278
+
279
+ .explanation-card-header {
280
+ font-family: 'Google Sans', sans-serif;
281
+ font-size: 14px;
282
+ font-weight: 600;
283
+ color: var(--sushruta-accent-blue);
284
+ text-transform: uppercase;
285
+ letter-spacing: 0.8px;
286
+ margin-bottom: 12px;
287
+ }
288
+
289
+ .explanation-text {
290
+ font-size: 15px;
291
+ line-height: 1.6;
292
+ color: var(--sushruta-text-primary);
293
+ margin: 0;
294
+ }
295
+
296
+ .generating-indicator {
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 12px;
300
+ color: var(--sushruta-accent-blue);
301
+ font-size: 14px;
302
+ }
303
+
304
+ .pulse-dot {
305
+ width: 10px;
306
+ height: 10px;
307
+ border-radius: 50%;
308
+ background: var(--sushruta-accent-blue);
309
+ animation: glowPulse 1.2s infinite alternate;
310
+ }
311
+
312
+ @keyframes glowPulse {
313
+ 0% {
314
+ transform: scale(0.8);
315
+ box-shadow: 0 0 0 rgba(79, 143, 255, 0.5);
316
+ }
317
+ 100% {
318
+ transform: scale(1.2);
319
+ box-shadow: 0 0 10px rgba(79, 143, 255, 0.8);
320
+ }
321
+ }
322
+
323
+ /* Report Text Card */
324
+ .report-text-card {
325
+ background: var(--sushruta-bg-card);
326
+ border: 1px solid rgba(255, 255, 255, 0.08);
327
+ border-radius: 16px;
328
+ display: flex;
329
+ flex-direction: column;
330
+ flex-grow: 1;
331
+ min-height: 250px;
332
+ }
333
+
334
+ .report-card-header {
335
+ background: rgba(255, 255, 255, 0.02);
336
+ padding: 12px 20px;
337
+ font-family: 'Google Sans', sans-serif;
338
+ font-size: 14px;
339
+ font-weight: 600;
340
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
341
+ color: var(--sushruta-text-secondary);
342
+ }
343
+
344
+ .report-body {
345
+ padding: 20px;
346
+ overflow-y: auto;
347
+ flex-grow: 1;
348
+ }
349
+
350
+ .report-paragraph {
351
+ margin-bottom: 16px;
352
+ line-height: 1.7;
353
+ }
354
+
355
+ .report-sentence {
356
+ cursor: pointer;
357
+ padding: 2px 4px;
358
+ border-radius: 4px;
359
+ transition: all 0.2s ease;
360
+ }
361
+
362
+ .report-sentence:hover {
363
+ background: rgba(255, 255, 255, 0.06);
364
+ color: #ffffff;
365
+ }
366
+
367
+ .report-sentence.selected {
368
+ background: rgba(0, 212, 170, 0.15);
369
+ border-left: 2px solid var(--sushruta-accent-teal);
370
+ color: #ffffff;
371
+ font-weight: 500;
372
+ }
373
+
374
+ .placeholder-text {
375
+ color: #555565;
376
+ font-style: italic;
377
+ font-size: 14px;
378
+ }
379
+
380
+ /* Footer & Disclaimer */
381
+ .radiology-footer {
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: center;
385
+ gap: 12px;
386
+ padding: 16px 32px;
387
+ background: rgba(255, 255, 255, 0.02);
388
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
389
+ z-index: 1;
390
+ }
391
+
392
+ .warning-icon {
393
+ color: #d4a373;
394
+ font-size: 20px;
395
+ }
396
+
397
+ .radiology-footer .disclaimer-text {
398
+ font-size: 12px;
399
+ color: #8a8a9a;
400
+ margin: 0;
401
+ line-height: 1.4;
402
+ }
403
+
404
+ /* Small Loaders */
405
+ .small-loader {
406
+ border: 2px solid rgba(255, 255, 255, 0.1);
407
+ border-radius: 50%;
408
+ border-top: 2px solid var(--sushruta-accent-teal);
409
+ width: 20px;
410
+ height: 20px;
411
+ animation: spin 1s linear infinite;
412
+ margin: 10px auto;
413
+ }
414
+
415
+ @keyframes spin {
416
+ 0% { transform: rotate(0deg); }
417
+ 100% { transform: rotate(360deg); }
418
+ }
419
+
420
+ @keyframes pulse {
421
+ 0% { opacity: 0.6; }
422
+ 50% { opacity: 1; }
423
+ 100% { opacity: 0.6; }
424
+ }
frontend/src/components/RadiologyExplainer/RadiologyExplainer.js ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React, { useState, useEffect } from 'react';
18
+ import './RadiologyExplainer.css';
19
+ import DetailsPopup from '../DetailsPopup/DetailsPopup';
20
+
21
+ const RadiologyExplainer = ({ onBack }) => {
22
+ const [reports, setReports] = useState([]);
23
+ const [selectedReportName, setSelectedReportName] = useState('');
24
+ const [reportDetails, setReportDetails] = useState(null);
25
+ const [selectedSentence, setSelectedSentence] = useState('');
26
+ const [explanation, setExplanation] = useState('Click a sentence in the report to see the MedGemma explanation here.');
27
+ const [loadingReports, setLoadingReports] = useState(true);
28
+ const [loadingDetails, setLoadingDetails] = useState(false);
29
+ const [loadingExplanation, setLoadingExplanation] = useState(false);
30
+ const [error, setError] = useState(null);
31
+ const [showInfoPopup, setShowInfoPopup] = useState(false);
32
+
33
+ // 1. Fetch available reports on mount
34
+ useEffect(() => {
35
+ fetch('/api/radiology/reports')
36
+ .then((res) => {
37
+ if (!res.ok) throw new Error('Failed to load radiology cases.');
38
+ return res.json();
39
+ })
40
+ .then((data) => {
41
+ setReports(data);
42
+ setLoadingReports(false);
43
+ // Automatically select the first case if available
44
+ if (data.length > 0) {
45
+ setSelectedReportName(data[0].name);
46
+ }
47
+ })
48
+ .catch((err) => {
49
+ setError(err.message);
50
+ setLoadingReports(false);
51
+ });
52
+ }, []);
53
+
54
+ // 2. Fetch specific report details when selection changes
55
+ useEffect(() => {
56
+ if (!selectedReportName) return;
57
+
58
+ setLoadingDetails(true);
59
+ setReportDetails(null);
60
+ setSelectedSentence('');
61
+ setExplanation('Click a sentence in the report to see the MedGemma explanation here.');
62
+ setError(null);
63
+
64
+ fetch(`/api/radiology/report/${encodeURIComponent(selectedReportName)}`)
65
+ .then((res) => {
66
+ if (!res.ok) throw new Error('Failed to load report details.');
67
+ return res.json();
68
+ })
69
+ .then((data) => {
70
+ setReportDetails(data);
71
+ setLoadingDetails(false);
72
+ })
73
+ .catch((err) => {
74
+ setError(err.message);
75
+ setLoadingDetails(false);
76
+ });
77
+ }, [selectedReportName]);
78
+
79
+ // 3. Handle sentence click (explain)
80
+ const handleSentenceClick = (sentence) => {
81
+ if (!sentence || !sentence.trim()) return;
82
+ setSelectedSentence(sentence);
83
+ setLoadingExplanation(true);
84
+ setExplanation('');
85
+ setError(null);
86
+
87
+ fetch('/api/radiology/explain', {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({
91
+ sentence: sentence.trim(),
92
+ report_name: selectedReportName,
93
+ }),
94
+ })
95
+ .then((res) => {
96
+ if (!res.ok) throw new Error('Failed to generate explanation.');
97
+ return res.json();
98
+ })
99
+ .then((data) => {
100
+ setExplanation(data.explanation);
101
+ setLoadingExplanation(false);
102
+ })
103
+ .catch((err) => {
104
+ setExplanation('Failed to generate explanation. Please try again.');
105
+ setLoadingExplanation(false);
106
+ });
107
+ };
108
+
109
+ // Helper to split paragraph into sentences
110
+ const splitSentences = (text) => {
111
+ if (!text) return [];
112
+ // Basic medical sentence splitting (handles dots after numbers/caps gracefully)
113
+ const sentences = text.match(/[^.!?]+[.!?]+(\s+|$)/g) || [text];
114
+ return sentences.map((s) => s.trim()).filter(Boolean);
115
+ };
116
+
117
+ const getModalityHeader = () => {
118
+ if (!reportDetails) return 'Medical Image';
119
+ return reportDetails.image_type || 'Medical Image';
120
+ };
121
+
122
+ const cxrCases = reports.filter((r) => r.image_type === 'CXR');
123
+ const ctCases = reports.filter((r) => r.image_type === 'CT');
124
+
125
+ return (
126
+ <div className="radiology-explainer">
127
+ {/* Background glow effects */}
128
+ <div className="radiology-orb-left"></div>
129
+ <div className="radiology-orb-right"></div>
130
+
131
+ {/* Top Navbar */}
132
+ <nav className="radiology-nav">
133
+ <button className="back-button" onClick={onBack} id="back-button-landing">
134
+ <span className="material-icons back-button-icon">keyboard_arrow_left</span>
135
+ <span>Back</span>
136
+ </button>
137
+
138
+ <h1 className="radiology-logo">Radiology Explainer</h1>
139
+
140
+ <button className="details-button" onClick={() => setShowInfoPopup(true)} id="info-button-details">
141
+ <span className="material-icons code-block-icon">code</span>
142
+ <span>Demo Details</span>
143
+ </button>
144
+ </nav>
145
+
146
+ {/* Main Content Area */}
147
+ <div className="radiology-container">
148
+ {/* Left Panel: Case Selection */}
149
+ <aside className="radiology-sidebar">
150
+ <div className="sidebar-group">
151
+ <h3>Chest X-Ray Cases</h3>
152
+ <div className="case-buttons">
153
+ {loadingReports ? (
154
+ <div className="small-loader"></div>
155
+ ) : cxrCases.length > 0 ? (
156
+ cxrCases.map((c) => (
157
+ <button
158
+ key={c.name}
159
+ className={`case-btn ${selectedReportName === c.name ? 'active' : ''}`}
160
+ onClick={() => setSelectedReportName(c.name)}
161
+ >
162
+ {c.name}
163
+ </button>
164
+ ))
165
+ ) : (
166
+ <span className="no-cases">No X-Ray cases</span>
167
+ )}
168
+ </div>
169
+ </div>
170
+
171
+ <div className="sidebar-group">
172
+ <h3>CT scan Cases</h3>
173
+ <div className="case-buttons">
174
+ {loadingReports ? (
175
+ <div className="small-loader"></div>
176
+ ) : ctCases.length > 0 ? (
177
+ ctCases.map((c) => (
178
+ <button
179
+ key={c.name}
180
+ className={`case-btn ${selectedReportName === c.name ? 'active' : ''}`}
181
+ onClick={() => setSelectedReportName(c.name)}
182
+ >
183
+ {c.name}
184
+ </button>
185
+ ))
186
+ ) : (
187
+ <span className="no-cases">No CT cases</span>
188
+ )}
189
+ </div>
190
+ </div>
191
+ </aside>
192
+
193
+ {/* Center Panel: Image Viewer */}
194
+ <main className="radiology-main">
195
+ <div className="image-card">
196
+ <div className="image-card-header">{getModalityHeader()}</div>
197
+ <div className="image-viewer-container">
198
+ {loadingDetails ? (
199
+ <div className="image-loading">Loading medical image...</div>
200
+ ) : reportDetails?.image_file ? (
201
+ <img
202
+ src={`/${reportDetails.image_file}`}
203
+ alt="Radiology Study"
204
+ className="radiology-image"
205
+ />
206
+ ) : (
207
+ <div className="image-placeholder">No image selected</div>
208
+ )}
209
+ </div>
210
+ {reportDetails?.image_type === 'CT' && (
211
+ <div className="image-footer-note">
212
+ * Note: Displays a single high-yield slice of the complete CT scan.
213
+ </div>
214
+ )}
215
+ </div>
216
+ </main>
217
+
218
+ {/* Right Panel: Report & Explanations */}
219
+ <section className="radiology-report-panel">
220
+ {/* Explanation Panel */}
221
+ <div className="explanation-card">
222
+ <div className="explanation-card-header">Clinical Explanation</div>
223
+ <div className="explanation-body">
224
+ {loadingExplanation ? (
225
+ <div className="generating-indicator">
226
+ <div className="pulse-dot"></div>
227
+ <span>MedGemma translating sentence...</span>
228
+ </div>
229
+ ) : (
230
+ <p className="explanation-text">{explanation}</p>
231
+ )}
232
+ </div>
233
+ </div>
234
+
235
+ {/* Report Text Display */}
236
+ <div className="report-text-card">
237
+ <div className="report-card-header">Radiology Report</div>
238
+ <div className="report-body">
239
+ {loadingDetails ? (
240
+ <div className="small-loader"></div>
241
+ ) : error ? (
242
+ <div className="error-text">{error}</div>
243
+ ) : reportDetails?.text ? (
244
+ <div className="report-paragraphs">
245
+ {reportDetails.text.split('\n\n').map((paragraph, pIdx) => (
246
+ <p key={pIdx} className="report-paragraph">
247
+ {splitSentences(paragraph).map((sentence, sIdx) => {
248
+ const isSelected = selectedSentence === sentence;
249
+ return (
250
+ <span
251
+ key={sIdx}
252
+ className={`report-sentence ${isSelected ? 'selected' : ''}`}
253
+ onClick={() => handleSentenceClick(sentence)}
254
+ >
255
+ {sentence}{' '}
256
+ </span>
257
+ );
258
+ })}
259
+ </p>
260
+ ))}
261
+ </div>
262
+ ) : (
263
+ <div className="placeholder-text">Select a report to view text</div>
264
+ )}
265
+ </div>
266
+ </div>
267
+ </section>
268
+ </div>
269
+
270
+ {/* Floating Disclaimer */}
271
+ <footer className="radiology-footer">
272
+ <span className="material-icons warning-icon">warning</span>
273
+ <p className="disclaimer-text">
274
+ <strong>Demo Disclaimer:</strong> MedGemma explains medical concepts for informational purposes only. Do not use for clinical diagnostics or medical decisions.
275
+ </p>
276
+ </footer>
277
+
278
+ {/* Info Details Modal */}
279
+ {showInfoPopup && (
280
+ <DetailsPopup isOpen={true} onClose={() => setShowInfoPopup(false)} />
281
+ )}
282
+ </div>
283
+ );
284
+ };
285
+
286
+ export default RadiologyExplainer;
frontend/src/components/RolePlayDialogs/RolePlayDialogs.css ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ .frame.role-play-container {
18
+ display: grid;
19
+ justify-items: center;
20
+ align-content: center;
21
+ align-items: center;
22
+ grid-gap: 30px;
23
+ width: fit-content;
24
+ align-self: center;
25
+ }
26
+
27
+ .dialogs-container {
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ gap: 20px;
32
+ margin-top: 50px;
33
+ }
34
+
35
+
36
+ .dialog-box {
37
+ border-radius: 5.667px;
38
+ border: 1.889px solid #E9E9E9;
39
+ background: #FFF;
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ width: 477px;
44
+ }
45
+
46
+ .dialog-title-text {
47
+ padding-top: 24px;
48
+ font-size: 1.6rem;
49
+ font-weight: 500;
50
+ color: #202124;
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 10px;
54
+ }
55
+
56
+ .dialog-body-scrollable {
57
+ padding: 16px;
58
+ overflow-y: auto;
59
+ flex-grow: 1;
60
+ color: #3c4043;
61
+ line-height: 1.6;
62
+ }
63
+
64
+ .dialog-subtitle {
65
+ font-weight: 500;
66
+ margin-bottom: 8px;
67
+ }
68
+
69
+ .variable {
70
+ color: #e81ad7;
71
+ font-weight: bold;
72
+ }
73
+
74
+ .patient-avatar {
75
+ border-radius: 50%;
76
+ margin: 0 10px;
77
+ width: 90px;
78
+ height: 90px;
79
+ }
80
+
81
+ .ai-avatar {
82
+ border-radius: 50%;
83
+ width: 90px;
84
+ height: 90px;
85
+ background: #E8DEF8;
86
+ }
87
+
88
+ .report-notice {
89
+ width: 974px;
90
+ }
91
+
92
+ .highlight {
93
+ background-color: #E8DEF8;
94
+ font-weight: 700;
95
+ padding: 0 4px;
96
+ border-radius: 8px;
97
+ }
98
+
99
+ .role-play-container .info-button {
100
+ justify-self: flex-start;
101
+ }
frontend/src/components/RolePlayDialogs/RolePlayDialogs.js ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React, { useState } from "react";
18
+ import "./RolePlayDialogs.css";
19
+ import DetailsPopup from "../DetailsPopup/DetailsPopup";
20
+
21
+ const RolePlayDialogs = ({
22
+ selectedPatient,
23
+ selectedCondition,
24
+ onStart,
25
+ onBack,
26
+ }) => {
27
+ const [isDetailsPopupOpen, setIsDetailsPopupOpen] = useState(false);
28
+
29
+ return (
30
+ <div className="page">
31
+ <div className="headerButtonsContainer">
32
+ <button className="back-button" onClick={onBack}>
33
+ <i className="material-icons back-button-icon">keyboard_arrow_left</i>
34
+ Back
35
+ </button>
36
+ <button className="details-button" onClick={() => setIsDetailsPopupOpen(true)}>
37
+ <i className="material-icons code-block-icon">code</i>&nbsp; Details
38
+ about this Demo
39
+ </button>
40
+ </div>
41
+ <div className="frame role-play-container">
42
+ <div className="title-header">What’s happening in this simulation</div>
43
+ <div className="dialogs-container">
44
+ <div className="dialog-box">
45
+ <div className="dialog-title-text">Pre-visit AI agent</div>
46
+ <div className="dialog-subtitle">
47
+ Built with: <img src="assets/medgemma.avif" height="16px" />{" "}
48
+ 27b
49
+ </div>
50
+ <img
51
+ src="assets/ai_headshot.svg"
52
+ alt="AI Avatar"
53
+ className="ai-avatar"
54
+ />
55
+ <div className="dialog-body-scrollable">
56
+ In this demo, MedGemma functions as an AI agent designed to assist in pre-visit information
57
+ collection. It will interact with the patient agent to gather relevant data.
58
+ To provide additional context, MedGemma also has access to information from the patient's EHR (in FHIR format).
59
+ However, MedGemma is not provided the specific diagnois ({selectedCondition}).
60
+ MedGemma's goal is to gather details about symptoms, relevant history,
61
+ and current concerns to generate a comprehensive pre-visit report.
62
+ </div>
63
+ </div>
64
+ <div className="dialog-box">
65
+ <div className="dialog-title-text">
66
+ Patient persona: {selectedPatient.name}
67
+ </div>
68
+ <div className="dialog-subtitle">
69
+ Simulated by:{" "}Gemini 2.5 Flash
70
+ </div>
71
+ <img
72
+ src={selectedPatient.headshot}
73
+ alt="Patient Avatar"
74
+ className="patient-avatar"
75
+ />
76
+ <div className="dialog-body-scrollable">
77
+ Gemini is provided a persona and information to play the role of the patient, {selectedPatient.name}.
78
+ In this simulation, the patient agent does not know their diagnosis,
79
+ but is experiencing related symptoms and concerns that can be shared during the interview.
80
+ To simulate a real-world situation with confounding information, additional information unrelated to the presenting condition has also been provided.
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <div className="report-notice">
85
+ As the conversation develops, MedGemma <span className="highlight">creates and continually updates
86
+ a real-time pre-visit report</span> capturing relevant
87
+ information. Following pre-visit report generation, an evaluation is available. The purpose of this evaluation is to provide the viewer insights into quality of the output.
88
+ For this evaluation, MedGemma is provided the previously unknown reference diagnosis, and is prompted to generate a
89
+ <span className="highlight">self evaluation that highlights strengths as well opportunities where the conversation and report could have been improved.</span>
90
+ </div>
91
+ <button className="info-button" onClick={onStart}>
92
+ Start conversation
93
+ </button>
94
+ </div>
95
+ <DetailsPopup
96
+ isOpen={isDetailsPopupOpen}
97
+ onClose={() => setIsDetailsPopupOpen(false)}
98
+ />
99
+ </div>
100
+ );
101
+ };
102
+
103
+ export default RolePlayDialogs;
frontend/src/components/WelcomePage/WelcomePage.css ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ body:has(.welcome) {
18
+ background-color: white;
19
+ }
20
+
21
+ .info-page-container {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ gap: 40px;
26
+ padding: 40px;
27
+ max-width: 1300px;
28
+ margin: auto;
29
+ }
30
+
31
+ .info-content {
32
+ flex: 1;
33
+ min-width: 500px;
34
+ max-width: 1000px;
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 20px;
38
+ font-size: 18px;
39
+ }
40
+
41
+ .info-header {
42
+ margin-bottom: 10px;
43
+ }
44
+
45
+ .title-header {
46
+ font-family: 'Google Sans', sans-serif;
47
+ font-size: 32px;
48
+ font-weight: 500;
49
+ font-style: normal;
50
+ }
51
+
52
+ .welcome .medgemma-logo {
53
+ width: 130px;
54
+ align-self: flex-end;
55
+ margin-top: 10px;
56
+ margin-right: 10px;
57
+ }
58
+
59
+ .info-button {
60
+ background-color: #C2E7FF;
61
+ }
62
+
63
+ .info-disclaimer-text {
64
+ font-family: 'Google Sans', sans-serif;
65
+ color: #333;
66
+ line-height: 1.5;
67
+ margin: 0;
68
+ font-size: 14px;
69
+ }
70
+
71
+ .info-disclaimer-title {
72
+ border-radius: 14.272px;
73
+ border: 1.359px solid #F1E161;
74
+ background: #F1E161;
75
+ mix-blend-mode: multiply;
76
+ padding: 0 5px;
77
+ }
78
+
79
+ .graphics {
80
+ position: relative;
81
+ min-width: 250px;
82
+ max-width: 450px;
83
+ flex: 0.5;
84
+ aspect-ratio: 1.2 / 1;
85
+ }
86
+
87
+ @media (max-width: 900px) {
88
+ .info-page-container {
89
+ flex-direction: column;
90
+ padding: 20px;
91
+ margin: 10px;
92
+ }
93
+
94
+ .info-content {
95
+ max-width: 100%;
96
+ align-items: center;
97
+ text-align: center;
98
+ }
99
+
100
+ .info-button {
101
+ align-self: center;
102
+ }
103
+
104
+ .info-header {
105
+ text-align: center;
106
+ }
107
+
108
+ .title-header {
109
+ font-size: 36px;
110
+ }
111
+
112
+ .info-text {
113
+ font-size: 16px;
114
+ }
115
+ .graphics {
116
+ min-width: 200px;
117
+ }
118
+ }
119
+
120
+
121
+
122
+ .graphics-top {
123
+ position: absolute;
124
+ top: 0;
125
+ left: 0;
126
+ z-index: 0;
127
+ width: 80%;
128
+ }
129
+
130
+ .graphics-bottom {
131
+ position: absolute;
132
+ bottom: 0;
133
+ right: 0;
134
+ z-index: 1;
135
+ opacity: 0;
136
+ animation: fadeIn 1s ease forwards;
137
+ width: 62%;
138
+ }
139
+
140
+ @keyframes fadeIn {
141
+ to {
142
+ opacity: 1;
143
+ }
144
+ }
frontend/src/components/WelcomePage/WelcomePage.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import './WelcomePage.css';
19
+
20
+ const WelcomePage = ({ onSwitchPage, onBack }) => {
21
+ return (
22
+ <div className="welcome page">
23
+ {onBack && (
24
+ <div className="headerButtonsContainer" style={{ padding: '20px 40px 0 40px' }}>
25
+ <button className="back-button" onClick={onBack} id="back-button-dashboard">
26
+ <span className="material-icons back-button-icon">keyboard_arrow_left</span>
27
+ <span>Back to Dashboard</span>
28
+ </button>
29
+ </div>
30
+ )}
31
+
32
+ <img src="/assets/medgemma.avif" alt="MedGemma Logo" className="medgemma-logo" />
33
+
34
+ <div className="info-page-container">
35
+ <div className="graphics">
36
+ <img className="graphics-top" src="/assets/welcome_top_graphics.svg" alt="Welcome top graphics" />
37
+ <img className="graphics-bottom" src="/assets/welcome_bottom_graphics.svg" alt="Welcome bottom graphics" />
38
+ </div>
39
+ <div className="info-content">
40
+ <div className="info-header">
41
+ <span className="title-header">Simulated Pre-visit Intake Demo</span>
42
+ </div>
43
+ <div className="info-text">
44
+ Healthcare providers often need to gather patient information before a visit.
45
+ This demo illustrates how MedGemma could be used in an application to streamline pre-visit information collection and utilization.
46
+ <br /><br/>
47
+ First, a pre-visit AI agent built with MedGemma asks questions to gather information.
48
+ After it has identified and collected relevant information, the demo application generates a pre-visit report.
49
+ <br /><br/>
50
+ This type of intelligent pre-visit report can help providers be more efficient and effective while also providing an improved experience
51
+ for patients relative to traditional intake forms.
52
+ <br /><br/>
53
+ Lastly, you can view an evaluation of the pre-visit report which provides insights into the quality of the output.
54
+ For this evaluation, MedGemma is provided the reference diagnosis, allowing "self-evaluation" that highlights both strengths and what it could have done better.
55
+ </div>
56
+ <div className="info-disclaimer-text">
57
+ <span className="info-disclaimer-title">Disclaimer</span> This
58
+ demonstration is for illustrative purposes only and does not represent a finished or approved
59
+ product. It is not representative of compliance to any regulations or standards for
60
+ quality, safety or efficacy. Any real-world application would require additional development,
61
+ training, and adaptation. The experience highlighted in this demo shows MedGemma's baseline
62
+ capability for the displayed task and is intended to help developers and users explore possible
63
+ applications and inspire further development.
64
+ </div>
65
+ <button className="info-button" onClick={onSwitchPage}>Select Patient</button>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ );
70
+ };
71
+
72
+ export default WelcomePage;
frontend/src/index.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import ReactDOM from 'react-dom';
19
+ import App from './App';
20
+ import './shared/Style.css';
21
+
22
+ const root = ReactDOM.createRoot(document.getElementById('root'));
23
+ root.render(<App />);
frontend/src/shared/Style.css ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ :root {
18
+ --sushruta-bg-primary: #0a0a1a;
19
+ --sushruta-bg-secondary: #1a1a2e;
20
+ --sushruta-bg-card: rgba(255, 255, 255, 0.05);
21
+ --sushruta-text-primary: #e0e0e0;
22
+ --sushruta-text-secondary: #a0a0b0;
23
+ --sushruta-accent-blue: #4f8fff;
24
+ --sushruta-accent-teal: #00d4aa;
25
+ --sushruta-accent-purple: #8b5cf6;
26
+ --sushruta-border-glow: rgba(79, 143, 255, 0.3);
27
+ }
28
+
29
+ * {
30
+ box-sizing: border-box;
31
+ }
32
+
33
+ html {
34
+ --image-fixed-width: 280px;
35
+ height: 100%;
36
+ }
37
+
38
+ #root {
39
+ height: 100%;
40
+ margin: auto;
41
+ width: 100%;
42
+ }
43
+
44
+ body {
45
+ font-family: "Google Sans Text", sans-serif;
46
+ line-height: 1.6;
47
+ background-color: #f4f4f4;
48
+ color: #333;
49
+ margin: 0;
50
+ display: flex;
51
+ flex-direction: column;
52
+ user-select: none;
53
+ height: 100%;
54
+ }
55
+
56
+ .page {
57
+ display: flex;
58
+ flex-direction: column;
59
+ width: 100%;
60
+ height: fit-content;
61
+ }
62
+
63
+ .headerButtonsContainer {
64
+ display: flex;
65
+ justify-content: space-between;
66
+ width: -webkit-fill-available;
67
+ padding: 20px;
68
+ }
69
+
70
+ .info-button, .back-button, .details-button {
71
+ font-family: 'Google Sans Text', sans-serif;
72
+ font-size: 14px;
73
+ font-weight: 500;
74
+ padding: 6px 12px;
75
+ border-radius: 100px;
76
+ cursor: pointer;
77
+ text-align: center;
78
+ transition: background-color 0.3s ease;
79
+ align-self: flex-start;
80
+ border-width: 1px;
81
+ }
82
+
83
+ .back-button, .details-button {
84
+ padding: 8px 12px;
85
+ z-index: 10;
86
+ color: black;
87
+ background-color: transparent;
88
+ display: inline-flex;
89
+ align-items: center;
90
+ border-radius: 100px;
91
+ border: 1px solid rgba(196, 199, 197);
92
+ }
93
+
94
+ .back-button-icon {
95
+ margin-right: 4px;
96
+ }
97
+
98
+ .back-button-icon {
99
+ font-size: 14px;
100
+ }
101
+
102
+ .code-block-icon {
103
+ background-color: rgba(0, 74, 119);
104
+ color: rgba(194, 231, 255);
105
+ font-size: 14px;
106
+ }
107
+
108
+ .details-button {
109
+ background-color: rgba(194, 231, 255);
110
+ color: rgba(0, 74, 119);
111
+ border: none;
112
+ }
113
+
114
+ .info-button {
115
+ background-color: #0B57D0;
116
+ color: white;
117
+ padding: 12px;
118
+ }
119
+
120
+ .info-button:disabled {
121
+ background: #aaa;
122
+ cursor: not-allowed;
123
+ }
124
+
125
+ .info-button:hover:not(:disabled) {
126
+ background: #005fa3;
127
+ }
128
+
129
+ .frame {
130
+ border-radius: 28px;
131
+ border: 2px solid #E9E9E9;
132
+ background: #FFF;
133
+ padding: 20px 50px;
134
+ align-items: center;
135
+ display: flex;
136
+ flex-direction: column;
137
+ flex: 1;
138
+ justify-content: space-around;
139
+ margin: 0 10px;
140
+ min-height: 0;
141
+ width: fit-content;
142
+ }
143
+
144
+ .popup-overlay {
145
+ position: fixed;
146
+ top: 0;
147
+ left: 0;
148
+ width: 100%;
149
+ height: 100%;
150
+ background-color: rgba(0, 0, 0, 0.6);
151
+ display: flex;
152
+ justify-content: center;
153
+ align-items: center;
154
+ z-index: 1000;
155
+ backdrop-filter: blur(5px);
156
+ }
157
+
158
+ .popup-content {
159
+ background: #ffffff;
160
+ padding: 2rem;
161
+ border-radius: 12px;
162
+ max-width: 800px;
163
+ width: 90%;
164
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
165
+ border: 1px solid #e0e0e0;
166
+ animation: popup-fade-in 0.3s ease-out;
167
+ }
168
+
169
+ @keyframes popup-fade-in {
170
+ from {
171
+ opacity: 0;
172
+ transform: scale(0.95);
173
+ }
174
+ to {
175
+ opacity: 1;
176
+ transform: scale(1);
177
+ }
178
+ }
179
+
180
+ .popup-content h2 {
181
+ font-size: 1.5rem;
182
+ font-weight: 600;
183
+ color: #333;
184
+ margin-top: 0;
185
+ margin-bottom: 1rem;
186
+ text-align: center;
187
+ }
188
+
189
+ .popup-content p {
190
+ font-size: 1rem;
191
+ line-height: 1.6;
192
+ color: #555;
193
+ text-align: left;
194
+ margin-bottom: 1.5rem;
195
+ }
196
+
197
+ .popup-button {
198
+ display: block;
199
+ padding: 12px 20px;
200
+ font-size: 1rem;
201
+ font-weight: 600;
202
+ color: #fff;
203
+ background-color: #1a73e8;
204
+ border: none;
205
+ border-radius: 8px;
206
+ cursor: pointer;
207
+ transition: background-color 0.2s ease;
208
+ }
209
+
210
+ .popup-button:hover {
211
+ background-color: #185abc;
212
+ }
213
+
214
+ .hf-logo {
215
+ vertical-align: middle;
216
+ width: 30px;
217
+ }