Admin Idiakhoa commited on
Commit
db56fd6
·
1 Parent(s): 65d98e4

Add initial app code

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 +9 -0
  2. .gitattributes +3 -0
  3. .github/workflows/ci.yml +42 -0
  4. .gitignore +24 -0
  5. .pre-commit-config.yaml +24 -0
  6. Dockerfile +34 -0
  7. README.md +7 -6
  8. __init__.py +14 -0
  9. app.py +86 -0
  10. auth.py +77 -0
  11. cache.py +67 -0
  12. cache_archive.zip +3 -0
  13. evaluation.py +69 -0
  14. frontend/package-lock.json +0 -0
  15. frontend/package.json +31 -0
  16. frontend/public/assets/ai_headshot.svg +1 -0
  17. frontend/public/assets/alex.avif +0 -0
  18. frontend/public/assets/alex.mp4 +3 -0
  19. frontend/public/assets/alex_300.avif +0 -0
  20. frontend/public/assets/alex_fhir.json +246 -0
  21. frontend/public/assets/gemini.avif +0 -0
  22. frontend/public/assets/jason_fhir.json +167 -0
  23. frontend/public/assets/jordan.avif +0 -0
  24. frontend/public/assets/jordan.mp4 +3 -0
  25. frontend/public/assets/jordan_300.avif +0 -0
  26. frontend/public/assets/medgemma.avif +0 -0
  27. frontend/public/assets/patients_and_conditions.json +46 -0
  28. frontend/public/assets/sacha.avif +0 -0
  29. frontend/public/assets/sacha.mp4 +3 -0
  30. frontend/public/assets/sacha_150.avif +0 -0
  31. frontend/public/assets/sacha_fhir.json +266 -0
  32. frontend/public/assets/welcome_bottom_graphics.svg +1 -0
  33. frontend/public/assets/welcome_graphics.svg +1 -0
  34. frontend/public/assets/welcome_top_graphics.svg +1 -0
  35. frontend/public/index.html +29 -0
  36. frontend/src/App.js +88 -0
  37. frontend/src/components/DetailsPopup/DetailsPopup.css +46 -0
  38. frontend/src/components/DetailsPopup/DetailsPopup.js +76 -0
  39. frontend/src/components/Interview/Interview.css +282 -0
  40. frontend/src/components/Interview/Interview.js +491 -0
  41. frontend/src/components/PatientBuilder/PatientBuilder.css +205 -0
  42. frontend/src/components/PatientBuilder/PatientBuilder.js +235 -0
  43. frontend/src/components/PreloadImages.js +42 -0
  44. frontend/src/components/RolePlayDialogs/RolePlayDialogs.css +101 -0
  45. frontend/src/components/RolePlayDialogs/RolePlayDialogs.js +103 -0
  46. frontend/src/components/WelcomePage/WelcomePage.css +144 -0
  47. frontend/src/components/WelcomePage/WelcomePage.js +62 -0
  48. frontend/src/index.js +23 -0
  49. frontend/src/shared/Style.css +205 -0
  50. gemini.py +59 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ frontend/build
2
+ frontend/node_modules
3
+ env.list
4
+ __pycache__
5
+ .env*
6
+
7
+ # Git files
8
+ .git
9
+ .gitignore
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ frontend/public/assets/jordan.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ frontend/public/assets/sacha.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ frontend/public/assets/alex.mp4 filter=lfs diff=lfs merge=lfs -text
.github/workflows/ci.yml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python Application CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+
9
+ jobs:
10
+ lint-and-format:
11
+ name: Lint & Format Check
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: '3.9'
21
+
22
+ - name: Install pre-commit
23
+ run: pip install pre-commit
24
+
25
+ - name: Run linters and formatters
26
+ run: pre-commit run --all-files
27
+
28
+ test:
29
+ name: Run Backend Tests
30
+ runs-on: ubuntu-latest
31
+ needs: lint-and-format
32
+ steps:
33
+ - name: Checkout code
34
+ uses: actions/checkout@v4
35
+
36
+ - name: Set up Python and install dependencies
37
+ run: |
38
+ python -m pip install --upgrade pip
39
+ pip install -r requirements.txt -r requirements-dev.txt
40
+
41
+ - name: Run tests with pytest
42
+ run: pytest
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environments
2
+ .env
3
+ .env.*
4
+ !/.env.example
5
+ env.list
6
+
7
+ # Python dependencies and artifacts
8
+ .venv/
9
+ venv/
10
+ env/
11
+ __pycache__/
12
+ *.pyc
13
+
14
+ # Frontend dependencies and artifacts
15
+ frontend/node_modules/
16
+ frontend/build/
17
+ frontend/dist/
18
+ frontend/.pnp
19
+ frontend/.pnp.js
20
+ frontend/coverage/
21
+
22
+ # IDE / Editor specific
23
+ .vscode/
24
+ .idea/
.pre-commit-config.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://pre-commit.com for more information
2
+ # See https://pre-commit.com/hooks.html for more hooks
3
+ repos:
4
+ - repo: https://github.com/pre-commit/pre-commit-hooks
5
+ rev: v4.6.0
6
+ hooks:
7
+ - id: trailing-whitespace
8
+ - id: end-of-file-fixer
9
+ - id: check-yaml
10
+ - id: check-added-large-files
11
+
12
+ - repo: https://github.com/astral-sh/ruff-pre-commit
13
+ rev: v0.4.4
14
+ hooks:
15
+ - id: ruff
16
+ args: [--fix, --exit-non-zero-on-fix]
17
+ - id: ruff-format
18
+
19
+ - repo: https://github.com/pre-commit/mirrors-prettier
20
+ rev: v4.0.0-alpha.8
21
+ hooks:
22
+ - id: prettier
23
+ # Run prettier on frontend file types
24
+ types_or: [javascript, jsx, ts, tsx, css, scss, html, json, markdown]
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build the frontend
2
+ FROM node:18-alpine AS builder
3
+
4
+ # Set the working directory
5
+ WORKDIR /app/frontend
6
+
7
+ # Copy package files and install dependencies
8
+ COPY frontend/package.json frontend/package-lock.json ./
9
+ RUN npm install
10
+
11
+ # Copy the rest of the frontend code and build it
12
+ COPY frontend/ ./
13
+ RUN npm run build
14
+
15
+ # ---
16
+
17
+ # Stage 2: Build the final Python image
18
+ FROM python:3.9-slim
19
+
20
+ WORKDIR /app
21
+
22
+ # Copy Python dependency file and install dependencies
23
+ COPY requirements.txt .
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copy the built frontend assets from the 'builder' stage
27
+ COPY --from=builder /app/frontend/build ./static
28
+
29
+ # Copy your backend application code
30
+ COPY . .
31
+
32
+ # Expose the port your app will run on and define the startup command
33
+ EXPOSE 8000
34
+ CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "app.main:app", "--bind", "0.0.0.0:8000"]
README.md CHANGED
@@ -1,10 +1,11 @@
1
  ---
2
- title: Docappointemet
3
- emoji: 🐠
4
- colorFrom: gray
5
- colorTo: green
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: Appoint-Ready
3
+ emoji: 🗓️
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 8000
8
  ---
9
 
10
+ # Appoint-Ready
11
+ This is a web application for scheduling appointments.
__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
app.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from evaluation import evaluate_report, evaluation_prompt
16
+ from flask import Flask, send_from_directory, request, jsonify, Response, stream_with_context, send_file
17
+ from flask_cors import CORS
18
+ import os, time, json, re
19
+ from gemini import gemini_get_text_response
20
+ from interview_simulator import stream_interview
21
+ from cache import create_cache_zip
22
+ from medgemma import medgemma_get_text_response
23
+
24
+ app = Flask(__name__, static_folder=os.environ.get("FRONTEND_BUILD", "frontend/build"), static_url_path="/")
25
+ CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}})
26
+
27
+ @app.route("/")
28
+ def serve():
29
+ """Serves the main index.html file."""
30
+ return send_from_directory(app.static_folder, "index.html")
31
+
32
+
33
+ @app.route("/api/stream_conversation", methods=["GET"])
34
+ def stream_conversation():
35
+ """Streams the conversation with the interview simulator."""
36
+ patient = request.args.get("patient", "Patient")
37
+ condition = request.args.get("condition", "unknown condition")
38
+
39
+ def generate():
40
+ try:
41
+ for message in stream_interview(patient, condition):
42
+ yield f"data: {message}\n\n"
43
+ except Exception as e:
44
+ yield f"data: Error: {str(e)}\n\n"
45
+ raise e
46
+
47
+ return Response(stream_with_context(generate()), mimetype="text/event-stream")
48
+
49
+ @app.route("/api/evaluate_report", methods=["POST"])
50
+ def evaluate_report_call():
51
+ """Evaluates the provided medical report."""
52
+ data = request.get_json()
53
+ report = data.get("report", "")
54
+ if not report:
55
+ return jsonify({"error": "Report is required"}), 400
56
+ condition = data.get("condition", "")
57
+ if not condition:
58
+ return jsonify({"error": "Condition is required"}), 400
59
+
60
+ evaluation_text = evaluate_report(report, condition)
61
+
62
+ return jsonify({"evaluation": evaluation_text})
63
+
64
+
65
+ @app.route("/api/download_cache")
66
+ def download_cache_zip():
67
+ """Creates a zip file of the cache and returns it for download."""
68
+ zip_filepath, error = create_cache_zip()
69
+ if error:
70
+ return jsonify({"error": error}), 500
71
+ if not os.path.isfile(zip_filepath):
72
+ return jsonify({"error": f"File not found: {zip_filepath}"}), 404
73
+ return send_file(zip_filepath, as_attachment=True)
74
+
75
+
76
+ @app.route("/<path:path>")
77
+ def static_proxy(path):
78
+ """Serves static files and defaults to index.html for unknown paths."""
79
+ file_path = os.path.join(app.static_folder, path)
80
+ if os.path.isfile(file_path):
81
+ return send_from_directory(app.static_folder, path)
82
+ else:
83
+ return send_from_directory(app.static_folder, "index.html")
84
+
85
+ if __name__ == "__main__":
86
+ app.run(host="0.0.0.0", port=7860, threaded=True)
auth.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import json
16
+ import datetime
17
+ from google.oauth2 import service_account
18
+ import google.auth.transport.requests
19
+
20
+ def create_credentials(secret_key_json) -> service_account.Credentials:
21
+ """Creates Google Cloud credentials from the provided service account key.
22
+
23
+ Returns:
24
+ service_account.Credentials: The created credentials object.
25
+
26
+ Raises:
27
+ ValueError: If the environment variable is not set or is empty, or if the
28
+ JSON format is invalid.
29
+ """
30
+
31
+ if not secret_key_json:
32
+ raise ValueError("Userdata variable 'GCP_MEDGEMMA_SERVICE_ACCOUNT_KEY' is not set or is empty.")
33
+ try:
34
+ service_account_info = json.loads(secret_key_json)
35
+ except (SyntaxError, ValueError) as e:
36
+ raise ValueError("Invalid service account key JSON format.") from e
37
+ return service_account.Credentials.from_service_account_info(
38
+ service_account_info,
39
+ scopes=['https://www.googleapis.com/auth/cloud-platform']
40
+ )
41
+
42
+ def refresh_credentials(credentials: service_account.Credentials) -> service_account.Credentials:
43
+ """Refreshes the provided Google Cloud credentials if they are about to expire
44
+ (within 5 minutes) or if they don't have an expiry time set.
45
+
46
+ Args:
47
+ credentials: The credentials object to refresh.
48
+
49
+ Returns:
50
+ service_account.Credentials: The refreshed credentials object.
51
+ """
52
+ if credentials.expiry:
53
+ expiry_time = credentials.expiry.replace(tzinfo=datetime.timezone.utc)
54
+ # Calculate the time remaining until expiration
55
+ time_remaining = expiry_time - datetime.datetime.now(datetime.timezone.utc)
56
+ # Check if the token is about to expire (e.g., within 5 minutes)
57
+ if time_remaining < datetime.timedelta(minutes=5):
58
+ request = google.auth.transport.requests.Request()
59
+ credentials.refresh(request)
60
+ else:
61
+ # If no expiry is set, always attempt to refresh (e.g., for certain credential types)
62
+ request = google.auth.transport.requests.Request()
63
+ credentials.refresh(request)
64
+ return credentials
65
+
66
+ def get_access_token_refresh_if_needed(credentials: service_account.Credentials) -> str:
67
+ """Gets the access token from the credentials, refreshing them if needed.
68
+
69
+ Args:
70
+ credentials: The credentials object.
71
+
72
+ Returns:
73
+ str: The access token.
74
+ """
75
+ credentials = refresh_credentials(credentials)
76
+ return credentials.token
77
+
cache.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from diskcache import Cache
16
+ import os
17
+ import shutil
18
+ import tempfile
19
+ import zipfile
20
+ import logging
21
+
22
+ cache = Cache(os.environ.get("CACHE_DIR", "/cache"))
23
+ # Print cache statistics after loading
24
+ try:
25
+ item_count = len(cache)
26
+ size_bytes = cache.volume()
27
+ print(f"Cache loaded: {item_count} items, approx {size_bytes} bytes")
28
+ except Exception as e:
29
+ print(f"Could not retrieve cache statistics: {e}")
30
+
31
+ def create_cache_zip():
32
+ temp_dir = tempfile.gettempdir()
33
+ base_name = os.path.join(temp_dir, "cache_archive") # A more descriptive name
34
+ archive_path = base_name + ".zip"
35
+ cache_directory = os.environ.get("CACHE_DIR", "/cache")
36
+
37
+ if not os.path.isdir(cache_directory):
38
+ logging.error(f"Cache directory not found at {cache_directory}")
39
+ return None, f"Cache directory not found on server: {cache_directory}"
40
+
41
+ logging.info("Forcing a cache checkpoint for safe backup...")
42
+ try:
43
+ # Open and immediately close a connection.
44
+ # This forces SQLite to perform a checkpoint, merging the .wal file
45
+ # into the main .db file, ensuring the on-disk files are consistent.
46
+ with Cache(cache_directory) as temp_cache:
47
+ temp_cache.close()
48
+
49
+ # Clean up temporary files before archiving.
50
+ tmp_path = os.path.join(cache_directory, 'tmp')
51
+ if os.path.isdir(tmp_path):
52
+ logging.info(f"Removing temporary cache directory: {tmp_path}")
53
+ shutil.rmtree(tmp_path)
54
+
55
+ logging.info(f"Checkpoint complete. Creating zip archive of {cache_directory} to {archive_path}")
56
+ with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf:
57
+ for root, _, files in os.walk(cache_directory):
58
+ for file in files:
59
+ file_path = os.path.join(root, file)
60
+ arcname = os.path.relpath(file_path, cache_directory)
61
+ zipf.write(file_path, arcname)
62
+ logging.info("Zip archive created successfully.")
63
+ return archive_path, None
64
+
65
+ except Exception as e:
66
+ logging.error(f"Error creating zip archive of cache directory: {e}", exc_info=True)
67
+ return None, f"Error creating zip archive: {e}"
cache_archive.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7c4735cc77e6df31539abaa76ef2389252ab6057d875bba8a30b09fdaa5f84e0
3
+ size 6150757
evaluation.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import re
16
+ from medgemma import medgemma_get_text_response
17
+
18
+
19
+ def evaluation_prompt(defacto_condition):
20
+ # Returns a detailed prompt for the LLM to evaluate a pre-visit report for a specific condition
21
+ return f"""
22
+ Your role is to evaluate the helpfulness of a pre-visit report, which is based on a pre-visit patient interview and existing health records.
23
+ The patient was de facto diagnosed condition: "{defacto_condition}" which was not known at the time of the interview.
24
+
25
+ List the specific elements in the previsit report text that are helpful or necessary for the PCP to diagnose the de facto diagnosed condition: "{defacto_condition}".
26
+
27
+ This include pertinet positives or negatives.
28
+ List critical elements that are MISSING from the previsit report text that would have been helpful for the PCP to diagnose the de facto diagnosed condition.
29
+ This include pertinet positives or negatives that were missing from the report.
30
+ (keep in mind that the condition "{defacto_condition}" was not known at the time)
31
+
32
+ The evaluation output should be in HTML format.
33
+
34
+ REPORT TEMPLATE START
35
+
36
+ <h3 class="helpful">Helpful Facts:</h3>
37
+
38
+ <h3 class="missing">What wasn't covered but would be helpful:</h3>
39
+
40
+ REPORT TEMPLATE END
41
+ """
42
+
43
+ def evaluate_report(report, condition):
44
+ """Evaluate the pre-visit report based on the condition using MedGemma LLM."""
45
+ evaluation_text = medgemma_get_text_response([
46
+ {
47
+ "role": "system",
48
+ "content": [
49
+ {
50
+ "type": "text",
51
+ "text": f"{evaluation_prompt(condition)}"
52
+ }
53
+ ]
54
+ },
55
+ {
56
+ "role": "user",
57
+ "content": [
58
+ {
59
+ "type": "text",
60
+ "text": f"Here is the report text:\n{report}"
61
+ }
62
+ ]
63
+ },
64
+ ])
65
+
66
+ # Remove any LLM "thinking" blocks (special tokens sometimes present in output)
67
+ evaluation_text = re.sub(r'<unused94>.*?<unused95>', '', evaluation_text, flags=re.DOTALL)
68
+
69
+ return evaluation_text
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
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
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
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
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
frontend/public/assets/medgemma.avif ADDED
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
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
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": "[email protected]",
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,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>AppointReady</title>
22
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
24
+ </head>
25
+ <body>
26
+ <noscript>You need to enable JavaScript to run this app.</noscript>
27
+ <div id="root"></div>
28
+ </body>
29
+ </html>
frontend/src/App.js ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 WelcomePage from './components/WelcomePage/WelcomePage';
19
+ import PatientBuilder from './components/PatientBuilder/PatientBuilder';
20
+ import RolePlayDialogs from './components/RolePlayDialogs/RolePlayDialogs';
21
+ import Interview from './components/Interview/Interview';
22
+ import PreloadImages from './components/PreloadImages';
23
+
24
+ const App = () => {
25
+ const [currentPage, setCurrentPage] = useState('welcome');
26
+ const [selectedPatient, setSelectedPatient] = useState(null);
27
+ const [selectedCondition, setSelectedCondition] = useState(null);
28
+
29
+ const handleSwitchPage = () => {
30
+ setCurrentPage('patientBuilder');
31
+ };
32
+
33
+ const handleSwitchToRolePlayDialogs = () => {
34
+ setCurrentPage('rolePlayDialogs');
35
+ };
36
+
37
+ const handleSwitchToInterview = () => {
38
+ setCurrentPage('interview');
39
+ };
40
+
41
+ const imageList = [
42
+ '/assets/gemini.avif',
43
+ '/assets/medgemma.avif',
44
+ '/assets/ai_headshot.svg',
45
+ '/assets/jordan_300.avif',
46
+ '/assets/alex_300.avif',
47
+ '/assets/sacha_150.avif',
48
+ '/assets/jordan.avif',
49
+ '/assets/alex.avif',
50
+ '/assets/sacha.avif'
51
+ ];
52
+
53
+ return (
54
+ <PreloadImages imageSources={imageList}>
55
+ {currentPage === 'welcome' ? (
56
+ <WelcomePage
57
+ onSwitchPage={handleSwitchPage}
58
+ setSelectedPatient={setSelectedPatient}
59
+ setSelectedCondition={setSelectedCondition}
60
+ />
61
+ ) : currentPage === 'patientBuilder' ? (
62
+ <PatientBuilder
63
+ selectedPatient={selectedPatient}
64
+ selectedCondition={selectedCondition}
65
+ setSelectedPatient={setSelectedPatient}
66
+ setSelectedCondition={setSelectedCondition}
67
+ onNext={handleSwitchToRolePlayDialogs}
68
+ onBack={() => setCurrentPage('welcome')} // Back to WelcomePage
69
+ />
70
+ ) : currentPage === 'rolePlayDialogs' ? (
71
+ <RolePlayDialogs
72
+ selectedPatient={selectedPatient}
73
+ selectedCondition={selectedCondition}
74
+ onStart={handleSwitchToInterview}
75
+ onBack={() => setCurrentPage('patientBuilder')} // Back to PatientBuilder
76
+ />
77
+ ) : currentPage === 'interview' ? (
78
+ <Interview
79
+ selectedPatient={selectedPatient}
80
+ selectedCondition={selectedCondition}
81
+ onBack={() => setCurrentPage('rolePlayDialogs')} // Back to RolePlayDialogs
82
+ />
83
+ ) : null}
84
+ </PreloadImages>
85
+ );
86
+ };
87
+
88
+ 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,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
frontend/src/components/Interview/Interview.js ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ return isNowEnabled;
316
+ });
317
+ };
318
+
319
+ const playAudio = (audioDataUrl) => {
320
+ if (audioDataUrl) {
321
+ const audio = new Audio(audioDataUrl);
322
+ audio.play().catch(e => {
323
+ console.error("Error playing audio:", e);
324
+ });
325
+ }
326
+ };
327
+
328
+ return (
329
+ <div className="page interview-page">
330
+ <div className="headerButtonsContainer">
331
+ <button className="back-button" onClick={onBack}>
332
+ <i className="material-icons back-button-icon">keyboard_arrow_left</i>
333
+ Back
334
+ </button>
335
+ <button className="details-button" onClick={() => setIsDetailsPopupOpen(true)}>
336
+ <i className="material-icons code-block-icon">code</i>&nbsp; Details
337
+ about this Demo
338
+ </button>
339
+ </div>
340
+ <div className="frame">
341
+ <div className="interview-split-container">
342
+ {/* Top: Interview Chat */}
343
+ <div className="interview-left-section">
344
+ {/* Right: Chat */}
345
+ <div className="interview-chat-panel">
346
+ <div className="header2">
347
+ Simulated Interview
348
+ &nbsp;
349
+ <i
350
+ className="material-icons toggle-icon"
351
+ style={{
352
+ cursor: "pointer",
353
+ color: isAudioEnabled ? "#1976d2" : "#888",
354
+ }}
355
+ title={`Click to ${
356
+ isAudioEnabled ? "disable" : "enable"
357
+ } audio`}
358
+ onClick={handleToggleAudio}
359
+ >
360
+ {isAudioEnabled ? "volume_up" : "volume_off"}
361
+ </i>
362
+ {isAudioEnabled && (<span>audio by Gemini TTS</span>)}
363
+ {!isAudioEnabled && (
364
+ <i
365
+ className="material-icons toggle-icon"
366
+ style={{
367
+ cursor: "pointer",
368
+ color: waitTime === 1000 ? "#1976d2" : "#888",
369
+ }}
370
+ title={`Click to ${
371
+ waitTime === 1000 ? "slow down" : "speed up"
372
+ } the interview`}
373
+ onClick={handleToggleWaitTime}
374
+ >
375
+ speed
376
+ </i>)}
377
+
378
+ </div>
379
+ <div className="chat-container" ref={chatContainerRef}>
380
+ {messages.length === 0 ? (
381
+ <div className="chat-waiting-indicator">
382
+ Waiting for the interview to start...
383
+ </div>
384
+ ) : (
385
+ messages
386
+ .filter((msg) => msg.speaker !== "report")
387
+ .map((msg, idx, filteredMessages) => (
388
+ <div
389
+ ref={idx === filteredMessages.length - 1 ? lastMessageRef : null}
390
+ className={`chat-message-wrapper ${msg.speaker}${idx === filteredMessages.length - 1 ? " fade-in" : ""}${msg.audio ? " has-audio" : ""}`}
391
+ key={idx}
392
+ >
393
+ {msg.speaker.includes("interviewer") && (
394
+ <img
395
+ className="chat-avatar"
396
+ src="assets/ai_headshot.svg"
397
+ alt="Interviewer"
398
+ />
399
+ )}
400
+ <div className={`chat-bubble ${msg.audio ? "with-audio" : ""}`}>
401
+ {msg.speaker.includes("thinking") && (
402
+ <div className="thinking-header">Thinking...</div>
403
+ )}
404
+ {msg.text}
405
+ </div>
406
+ {msg.speaker === "patient" && (
407
+ <img
408
+ className="chat-avatar"
409
+ src={selectedPatient.headshot}
410
+ alt={selectedPatient.name}
411
+ />
412
+ )}
413
+ </div>
414
+ ))
415
+ )}
416
+ </div>
417
+ </div>
418
+ </div>
419
+ {/* Right: Report Section */}
420
+ <div className="interview-right-section">
421
+ <div className="header2">Generated Report</div>
422
+ <div className="report-content" ref={reportContentRef}>
423
+ {/* Updated report rendering to show diff if available */}
424
+ <div
425
+ dangerouslySetInnerHTML={{
426
+ __html: prevReport ? getDiffReport() : currentReport,
427
+ }}
428
+ />
429
+ {isInterviewComplete && (
430
+ <button
431
+ className="evaluate-button"
432
+ onClick={() => setShowEvaluationInfoPopup(true)}
433
+ disabled={showEvaluation || showEvaluationInfoPopup}
434
+ ><i className="material-icons back-button-icon">keyboard_arrow_down</i>
435
+ View Report Evaluation
436
+ </button>
437
+ )}
438
+ <div className="evaluation-text">
439
+ {showEvaluation && (
440
+ isFetchingEvaluation
441
+ ? <div>Please wait...</div>
442
+ : parse(evaluation)
443
+ )}
444
+ </div>
445
+ </div>
446
+ <div className="disclaimer-container">
447
+ <i className="material-icons warning-icon">warning</i>
448
+ <div className="disclaimer-text">
449
+ 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.
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+ {showEvaluationInfoPopup && (
456
+ <div className="popup-overlay">
457
+ <div className="popup-content">
458
+ <h2>About the Evaluation</h2>
459
+ <p>
460
+ Now we will ask MedGemma to evaluate its own performance at
461
+ generating this report. We will provide it with all the
462
+ information about {selectedPatient.name}, including their actual
463
+ diagnosis and aspects of condition history not included previously.
464
+ Using this new information, MedGemma will
465
+ highlight key facts it correctly included and identify other
466
+ information that would have been beneficial to add.
467
+ </p>
468
+ <p>
469
+ The purpose of this step is to provide non-medical users with a
470
+ sense of how well MedGemma did at this task. While the evaluation
471
+ is completed by MedGemma, the examples in this demo have also been
472
+ reviewed by clinicians for accuracy. Although MedGemma's evaluation
473
+ does not represent a consensus based standard,
474
+ this illustration simply shows an example of one approach developers could adopt
475
+ to evaluate quality and completeness.
476
+ </p>
477
+ <button className="popup-button" onClick={() => {
478
+ setShowEvaluationInfoPopup(false);
479
+ setShowEvaluation(true);
480
+ }}>Continue</button>
481
+ </div>
482
+ </div>)}
483
+ <DetailsPopup
484
+ isOpen={isDetailsPopupOpen}
485
+ onClose={() => setIsDetailsPopupOpen(false)}
486
+ />
487
+ </div>
488
+ );
489
+ };
490
+
491
+ export default Interview;
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,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ setSelectedCondition(null);
71
+ }
72
+ }, [selectedPatient]);
73
+
74
+ // When a new patient is selected, set the video to a loading state
75
+ // to ensure the placeholder image is shown.
76
+ useEffect(() => {
77
+ if (selectedPatient) {
78
+ setIsVideoLoading(true);
79
+ }
80
+ }, [selectedPatient]);
81
+
82
+ const handleGo = () => {
83
+ if (selectedPatient && selectedCondition) {
84
+ onNext();
85
+ }
86
+ };
87
+
88
+ const openPopup = (patient) => {
89
+ if (patient && patient.fhirFile) {
90
+ fetch(patient.fhirFile)
91
+ .then((response) => response.json())
92
+ .then((json) => {
93
+ setPopupJson(json);
94
+ setIsPopupOpen(true);
95
+ })
96
+ .catch((error) => console.error("Error fetching FHIR JSON:", error));
97
+ }
98
+ };
99
+
100
+ const closePopup = () => {
101
+ setIsPopupOpen(false);
102
+ setPopupJson(null);
103
+ };
104
+
105
+ return (
106
+ <div className="patient-builder-container">
107
+ <div className="headerButtonsContainer">
108
+ <button className="back-button" onClick={onBack}>
109
+ <i className="material-icons back-button-icon">keyboard_arrow_left</i>
110
+ Back
111
+ </button>
112
+ <button className="details-button" onClick={() => setIsDetailsPopupOpen(true)}>
113
+ <i className="material-icons code-block-icon">code</i>&nbsp;
114
+ Details about this Demo
115
+ </button>
116
+ </div>
117
+ <div className="frame">
118
+ <div className="selection-section">
119
+ <div className="header2">Select a Patient</div>
120
+ <div className="patient-list">
121
+ {patients.map((patient) => {
122
+ const isSelected = selectedPatient && selectedPatient.id === patient.id;
123
+ return (
124
+ <div
125
+ key={patient.id}
126
+ className="patient-card"
127
+ >
128
+ <div
129
+ className={`patient-video-container ${isSelected ? "selected" : ""}`}
130
+ onClick={() => setSelectedPatient(patient)}
131
+ >
132
+ <img
133
+ src={patient.img}
134
+ className="patient-img"
135
+ alt={patient.name}
136
+ draggable="false"
137
+ onDragStart={(e) => e.preventDefault()}
138
+ style={{ opacity: isSelected && !isVideoLoading ? 0 : 1 }}
139
+ />
140
+ {isSelected && (
141
+ <video
142
+ key={patient.id}
143
+ src={patient.video}
144
+ className="patient-video"
145
+ autoPlay
146
+ muted
147
+ loop
148
+ onCanPlay={() => setIsVideoLoading(false)}
149
+ style={{ opacity: isVideoLoading ? 0 : 1 }}
150
+ />
151
+ )}
152
+ <div className="ehr-label" onClick={(e) => { e.stopPropagation(); openPopup(patient); }}>
153
+ Synthetic Health Record (FHIR)
154
+ </div>
155
+ </div>
156
+ <div className="patient-info">
157
+ <div className="category-value">
158
+ {patient.name}, {patient.age} years old, {patient.gender}
159
+ </div>
160
+ <div className="category-value">
161
+ Existing condition: {patient.existing_condition}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ );
166
+ })}
167
+ </div>
168
+ </div>
169
+ <div className="selection-section">
170
+ <div className="header2">Explore a Condition</div>
171
+ <div className="lighttext">
172
+ In this demonstration, a persona, simulated using Gemini 2.5 Flash, will interact with an AI agent, built with MedGemma.
173
+ Neither the simulated persona nor the AI agent have been provided the diagnosis for the current condition (selected below).
174
+ The AI agent facilitates structured information-gathering, designed to usefully collect and summarize the patient's symptoms.
175
+ For the purposes of this demonstration, the AI agent also has access to elements of the patient's health record (provided as FHIR resources).
176
+ </div>
177
+ <div className="condition-list">
178
+ {conditions.map((cond) => {
179
+ const isDisabled =
180
+ cond.name === "Serotonin Syndrome" &&
181
+ selectedPatient &&
182
+ selectedPatient.existing_condition !== "Depression";
183
+ return (
184
+ <div
185
+ key={cond.name}
186
+ className={`condition-card lighttext ${
187
+ selectedCondition === cond.name ? "selected" : ""
188
+ } ${isDisabled ? "disabled" : ""}`}
189
+ onClick={
190
+ !isDisabled
191
+ ? () => setSelectedCondition(cond.name)
192
+ : undefined
193
+ }
194
+ >
195
+ <div><strong>{cond.name}</strong></div>
196
+ <div>{cond.description}</div>
197
+ </div>
198
+ );
199
+ })}
200
+ </div>
201
+ </div>
202
+ <button
203
+ className="info-button"
204
+ onClick={handleGo}
205
+ disabled={!(selectedPatient && selectedCondition)}
206
+ >
207
+ Launch simulation
208
+ </button>
209
+ </div>
210
+ {isPopupOpen && (
211
+ <div className="popup-overlay" onClick={closePopup}>
212
+ <div
213
+ className="popup-content json-popup-content"
214
+ onClick={(e) => e.stopPropagation()}
215
+ >
216
+ <h2>Synthetic Electronic Health Record</h2>
217
+ <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>
218
+ <div className="json-viewer-container">
219
+ <JsonViewer value={popupJson} theme="monokai" />
220
+ </div>
221
+ <button className="popup-button" onClick={closePopup}>
222
+ Close
223
+ </button>
224
+ </div>
225
+ </div>
226
+ )}
227
+ <DetailsPopup
228
+ isOpen={isDetailsPopupOpen}
229
+ onClose={() => setIsDetailsPopupOpen(false)}
230
+ />
231
+ </div>
232
+ );
233
+ };
234
+
235
+ 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/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 opporutunities 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,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 }) => {
21
+ return (
22
+ <div className="welcome page">
23
+ <img src="/assets/medgemma.avif" alt="MedGemma Logo" className="medgemma-logo" />
24
+ <div className="info-page-container">
25
+ <div className="graphics">
26
+ <img className="graphics-top" src="/assets/welcome_top_graphics.svg" alt="Welcome top graphics" />
27
+ <img className="graphics-bottom" src="/assets/welcome_bottom_graphics.svg" alt="Welcome bottom graphics" />
28
+ </div>
29
+ <div className="info-content">
30
+ <div className="info-header">
31
+ <span className="title-header">Simulated Pre-visit Intake Demo</span>
32
+ </div>
33
+ <div className="info-text">
34
+ Healthcare providers often need to gather patient information before appointments.
35
+ This demo illustrates how MedGemma could be used in an application to streamline pre-visit information collection and utilization.
36
+ <br /><br/>
37
+ First, a pre-visit AI agent built with MedGemma asks questions to gather information.
38
+ After it has identified and collected relevant information, the demo application generates a pre-visit report.
39
+ <br /><br/>
40
+ This type of intelligent pre-visit report can help providers be more efficient and effective while also providing an improved experience
41
+ for patients relative to traditional intake forms.
42
+ <br /><br/>
43
+ Lastly, you can view an evaluation of the pre-visit report which provides insights into the quality of the output.
44
+ For this evaluation, MedGemma is provided the reference diagnosis, allowing "self-evaluation" that highlights both strengths and what it could have done better.
45
+ </div>
46
+ <div className="info-disclaimer-text">
47
+ <span className="info-disclaimer-title">Disclaimer</span> This
48
+ demonstration is for illustrative purposes only and does not represent a finished or approved
49
+ product. It is not representative of compliance to any regulations or standards for
50
+ quality, safety or efficacy. Any real-world application would require additional development,
51
+ training, and adaptation. The experience highlighted in this demo shows MedGemma's baseline
52
+ capability for the displayed task and is intended to help developers and users explore possible
53
+ applications and inspire further development.
54
+ </div>
55
+ <button className="info-button" onClick={onSwitchPage}>Select Patient</button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ );
60
+ };
61
+
62
+ 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,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
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ html {
22
+ --image-fixed-width: 280px;
23
+ height: 100%;
24
+ }
25
+
26
+ #root {
27
+ height: 100%;
28
+ margin: auto;
29
+ width: 100%;
30
+ }
31
+
32
+ body {
33
+ font-family: "Google Sans Text", sans-serif;
34
+ line-height: 1.6;
35
+ background-color: #f4f4f4;
36
+ color: #333;
37
+ margin: 0;
38
+ display: flex;
39
+ flex-direction: column;
40
+ user-select: none;
41
+ height: 100%;
42
+ }
43
+
44
+ .page {
45
+ display: flex;
46
+ flex-direction: column;
47
+ width: 100%;
48
+ height: fit-content;
49
+ }
50
+
51
+ .headerButtonsContainer {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ width: -webkit-fill-available;
55
+ padding: 20px;
56
+ }
57
+
58
+ .info-button, .back-button, .details-button {
59
+ font-family: 'Google Sans Text', sans-serif;
60
+ font-size: 14px;
61
+ font-weight: 500;
62
+ padding: 6px 12px;
63
+ border-radius: 100px;
64
+ cursor: pointer;
65
+ text-align: center;
66
+ transition: background-color 0.3s ease;
67
+ align-self: flex-start;
68
+ border-width: 1px;
69
+ }
70
+
71
+ .back-button, .details-button {
72
+ padding: 8px 12px;
73
+ z-index: 10;
74
+ color: black;
75
+ background-color: transparent;
76
+ display: inline-flex;
77
+ align-items: center;
78
+ border-radius: 100px;
79
+ border: 1px solid rgba(196, 199, 197);
80
+ }
81
+
82
+ .back-button-icon {
83
+ margin-right: 4px;
84
+ }
85
+
86
+ .back-button-icon {
87
+ font-size: 14px;
88
+ }
89
+
90
+ .code-block-icon {
91
+ background-color: rgba(0, 74, 119);
92
+ color: rgba(194, 231, 255);
93
+ font-size: 14px;
94
+ }
95
+
96
+ .details-button {
97
+ background-color: rgba(194, 231, 255);
98
+ color: rgba(0, 74, 119);
99
+ border: none;
100
+ }
101
+
102
+ .info-button {
103
+ background-color: #0B57D0;
104
+ color: white;
105
+ padding: 12px;
106
+ }
107
+
108
+ .info-button:disabled {
109
+ background: #aaa;
110
+ cursor: not-allowed;
111
+ }
112
+
113
+ .info-button:hover:not(:disabled) {
114
+ background: #005fa3;
115
+ }
116
+
117
+ .frame {
118
+ border-radius: 28px;
119
+ border: 2px solid #E9E9E9;
120
+ background: #FFF;
121
+ padding: 20px 50px;
122
+ align-items: center;
123
+ display: flex;
124
+ flex-direction: column;
125
+ flex: 1;
126
+ justify-content: space-around;
127
+ margin: 0 10px;
128
+ min-height: 0;
129
+ width: fit-content;
130
+ }
131
+
132
+ .popup-overlay {
133
+ position: fixed;
134
+ top: 0;
135
+ left: 0;
136
+ width: 100%;
137
+ height: 100%;
138
+ background-color: rgba(0, 0, 0, 0.6);
139
+ display: flex;
140
+ justify-content: center;
141
+ align-items: center;
142
+ z-index: 1000;
143
+ backdrop-filter: blur(5px);
144
+ }
145
+
146
+ .popup-content {
147
+ background: #ffffff;
148
+ padding: 2rem;
149
+ border-radius: 12px;
150
+ max-width: 800px;
151
+ width: 90%;
152
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
153
+ border: 1px solid #e0e0e0;
154
+ animation: popup-fade-in 0.3s ease-out;
155
+ }
156
+
157
+ @keyframes popup-fade-in {
158
+ from {
159
+ opacity: 0;
160
+ transform: scale(0.95);
161
+ }
162
+ to {
163
+ opacity: 1;
164
+ transform: scale(1);
165
+ }
166
+ }
167
+
168
+ .popup-content h2 {
169
+ font-size: 1.5rem;
170
+ font-weight: 600;
171
+ color: #333;
172
+ margin-top: 0;
173
+ margin-bottom: 1rem;
174
+ text-align: center;
175
+ }
176
+
177
+ .popup-content p {
178
+ font-size: 1rem;
179
+ line-height: 1.6;
180
+ color: #555;
181
+ text-align: left;
182
+ margin-bottom: 1.5rem;
183
+ }
184
+
185
+ .popup-button {
186
+ display: block;
187
+ padding: 12px 20px;
188
+ font-size: 1rem;
189
+ font-weight: 600;
190
+ color: #fff;
191
+ background-color: #1a73e8;
192
+ border: none;
193
+ border-radius: 8px;
194
+ cursor: pointer;
195
+ transition: background-color 0.2s ease;
196
+ }
197
+
198
+ .popup-button:hover {
199
+ background-color: #185abc;
200
+ }
201
+
202
+ .hf-logo {
203
+ vertical-align: middle;
204
+ width: 30px;
205
+ }
gemini.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import os
16
+ import requests
17
+ from cache import cache # new import replacing duplicate cache initialization
18
+
19
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
20
+
21
+ # Decorate the function to cache its results indefinitely.
22
+ @cache.memoize()
23
+ def gemini_get_text_response(prompt: str,
24
+ stop_sequences: list = None,
25
+ temperature: float = 0.1,
26
+ max_output_tokens: int = 4000,
27
+ top_p: float = 0.8,
28
+ top_k: int = 10):
29
+ """
30
+ Makes a text generation request to the Gemini API.
31
+ """
32
+
33
+ api_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={GEMINI_API_KEY}"
34
+ headers = {
35
+ 'Content-Type': 'application/json'
36
+ }
37
+
38
+ data = {
39
+ "contents": [
40
+ {
41
+ "parts": [
42
+ {
43
+ "text": prompt
44
+ }
45
+ ]
46
+ }
47
+ ],
48
+ "generationConfig": {
49
+ "stopSequences": stop_sequences or ["Title"],
50
+ "temperature": temperature,
51
+ "maxOutputTokens": max_output_tokens,
52
+ "topP": top_p,
53
+ "topK": top_k
54
+ }
55
+ }
56
+
57
+ response = requests.post(api_url, headers=headers, json=data)
58
+ response.raise_for_status() # Raise an exception for bad status codes
59
+ return response.json()["candidates"][0]["content"]["parts"][0]["text"]