Spaces:
Sleeping
Sleeping
Commit ·
c2858c1
0
Parent(s):
Deploy EHRGym to Hugging Face Space
Browse files- .env.example +4 -0
- .gitignore +215 -0
- .vscode/settings.json +4 -0
- Dockerfile +25 -0
- LICENSE +201 -0
- README.md +357 -0
- apps/ehr/app/api/dev/reset/route.ts +15 -0
- apps/ehr/app/api/patients/[id]/route.ts +111 -0
- apps/ehr/app/api/patients/route.ts +35 -0
- apps/ehr/app/globals.css +1646 -0
- apps/ehr/app/layout.tsx +37 -0
- apps/ehr/app/page.tsx +234 -0
- apps/ehr/app/patient/[id]/actions.ts +105 -0
- apps/ehr/app/patient/[id]/page.tsx +455 -0
- apps/ehr/components/activity-nav.tsx +49 -0
- apps/ehr/components/app-brand.tsx +35 -0
- apps/ehr/components/chart-review-tabs.tsx +178 -0
- apps/ehr/components/section-card.tsx +23 -0
- apps/ehr/components/workspace-sidebar.tsx +179 -0
- apps/ehr/next-env.d.ts +6 -0
- apps/ehr/next.config.ts +7 -0
- apps/ehr/package.json +21 -0
- apps/ehr/tsconfig.json +17 -0
- docker-compose.yml +16 -0
- docker/Dockerfile +19 -0
- docker/entrypoint.sh +22 -0
- env_server/__init__.py +1 -0
- env_server/app/__init__.py +1 -0
- env_server/app/browser.py +95 -0
- env_server/app/main.py +154 -0
- env_server/app/models.py +54 -0
- package-lock.json +2249 -0
- package.json +29 -0
- prisma/schema.prisma +125 -0
- prisma/seed.ts +20 -0
- pyproject.toml +23 -0
- scripts/diagnose_server_actions.py +40 -0
- scripts/example_agent.py +33 -0
- scripts/measure_controls.py +26 -0
- scripts/ui_smoke_test.py +74 -0
- shared/reset-database.ts +82 -0
- shared/seed-data.ts +238 -0
- synthetic/README.md +5 -0
- tasks/examples/aki-chart-review.json +26 -0
- tsconfig.base.json +18 -0
- tsconfig.json +12 -0
.env.example
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DATABASE_URL="file:./dev.db"
|
| 2 |
+
EHR_BASE_URL="http://127.0.0.1:3000"
|
| 3 |
+
PLAYWRIGHT_HEADLESS="true"
|
| 4 |
+
OPENENV_DEFAULT_WAIT_MS="350"
|
.gitignore
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MacOS
|
| 2 |
+
.DS_Store
|
| 3 |
+
|
| 4 |
+
# Byte-compiled / optimized / DLL files
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[codz]
|
| 7 |
+
*$py.class
|
| 8 |
+
|
| 9 |
+
# C extensions
|
| 10 |
+
*.so
|
| 11 |
+
|
| 12 |
+
# Distribution / packaging
|
| 13 |
+
.Python
|
| 14 |
+
build/
|
| 15 |
+
node_modules/
|
| 16 |
+
.next/
|
| 17 |
+
*.tsbuildinfo
|
| 18 |
+
prisma/dev.db
|
| 19 |
+
prisma/dev.db-journal
|
| 20 |
+
develop-eggs/
|
| 21 |
+
dist/
|
| 22 |
+
downloads/
|
| 23 |
+
eggs/
|
| 24 |
+
.eggs/
|
| 25 |
+
lib/
|
| 26 |
+
lib64/
|
| 27 |
+
parts/
|
| 28 |
+
sdist/
|
| 29 |
+
var/
|
| 30 |
+
wheels/
|
| 31 |
+
share/python-wheels/
|
| 32 |
+
*.egg-info/
|
| 33 |
+
.installed.cfg
|
| 34 |
+
*.egg
|
| 35 |
+
MANIFEST
|
| 36 |
+
|
| 37 |
+
# PyInstaller
|
| 38 |
+
# Usually these files are written by a python script from a template
|
| 39 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 40 |
+
*.manifest
|
| 41 |
+
*.spec
|
| 42 |
+
|
| 43 |
+
# Installer logs
|
| 44 |
+
pip-log.txt
|
| 45 |
+
pip-delete-this-directory.txt
|
| 46 |
+
|
| 47 |
+
# Unit test / coverage reports
|
| 48 |
+
htmlcov/
|
| 49 |
+
.tox/
|
| 50 |
+
.nox/
|
| 51 |
+
.coverage
|
| 52 |
+
.coverage.*
|
| 53 |
+
.cache
|
| 54 |
+
nosetests.xml
|
| 55 |
+
coverage.xml
|
| 56 |
+
*.cover
|
| 57 |
+
*.py.cover
|
| 58 |
+
.hypothesis/
|
| 59 |
+
.pytest_cache/
|
| 60 |
+
cover/
|
| 61 |
+
|
| 62 |
+
# Translations
|
| 63 |
+
*.mo
|
| 64 |
+
*.pot
|
| 65 |
+
|
| 66 |
+
# Django stuff:
|
| 67 |
+
*.log
|
| 68 |
+
local_settings.py
|
| 69 |
+
db.sqlite3
|
| 70 |
+
db.sqlite3-journal
|
| 71 |
+
|
| 72 |
+
# Flask stuff:
|
| 73 |
+
instance/
|
| 74 |
+
.webassets-cache
|
| 75 |
+
|
| 76 |
+
# Scrapy stuff:
|
| 77 |
+
.scrapy
|
| 78 |
+
|
| 79 |
+
# Sphinx documentation
|
| 80 |
+
docs/_build/
|
| 81 |
+
|
| 82 |
+
# PyBuilder
|
| 83 |
+
.pybuilder/
|
| 84 |
+
target/
|
| 85 |
+
|
| 86 |
+
# Jupyter Notebook
|
| 87 |
+
.ipynb_checkpoints
|
| 88 |
+
|
| 89 |
+
# IPython
|
| 90 |
+
profile_default/
|
| 91 |
+
ipython_config.py
|
| 92 |
+
|
| 93 |
+
# pyenv
|
| 94 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 95 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 96 |
+
# .python-version
|
| 97 |
+
|
| 98 |
+
# pipenv
|
| 99 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 100 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 101 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 102 |
+
# install all needed dependencies.
|
| 103 |
+
#Pipfile.lock
|
| 104 |
+
|
| 105 |
+
# UV
|
| 106 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 107 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 108 |
+
# commonly ignored for libraries.
|
| 109 |
+
#uv.lock
|
| 110 |
+
|
| 111 |
+
# poetry
|
| 112 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 113 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 114 |
+
# commonly ignored for libraries.
|
| 115 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 116 |
+
#poetry.lock
|
| 117 |
+
#poetry.toml
|
| 118 |
+
|
| 119 |
+
# pdm
|
| 120 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 121 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 122 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 123 |
+
#pdm.lock
|
| 124 |
+
#pdm.toml
|
| 125 |
+
.pdm-python
|
| 126 |
+
.pdm-build/
|
| 127 |
+
|
| 128 |
+
# pixi
|
| 129 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 130 |
+
#pixi.lock
|
| 131 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 132 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 133 |
+
.pixi
|
| 134 |
+
|
| 135 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 136 |
+
__pypackages__/
|
| 137 |
+
|
| 138 |
+
# Celery stuff
|
| 139 |
+
celerybeat-schedule
|
| 140 |
+
celerybeat.pid
|
| 141 |
+
|
| 142 |
+
# SageMath parsed files
|
| 143 |
+
*.sage.py
|
| 144 |
+
|
| 145 |
+
# Environments
|
| 146 |
+
.env
|
| 147 |
+
.envrc
|
| 148 |
+
.venv
|
| 149 |
+
env/
|
| 150 |
+
venv/
|
| 151 |
+
ENV/
|
| 152 |
+
env.bak/
|
| 153 |
+
venv.bak/
|
| 154 |
+
|
| 155 |
+
# Spyder project settings
|
| 156 |
+
.spyderproject
|
| 157 |
+
.spyproject
|
| 158 |
+
|
| 159 |
+
# Rope project settings
|
| 160 |
+
.ropeproject
|
| 161 |
+
|
| 162 |
+
# mkdocs documentation
|
| 163 |
+
/site
|
| 164 |
+
|
| 165 |
+
# mypy
|
| 166 |
+
.mypy_cache/
|
| 167 |
+
.dmypy.json
|
| 168 |
+
dmypy.json
|
| 169 |
+
|
| 170 |
+
# Pyre type checker
|
| 171 |
+
.pyre/
|
| 172 |
+
|
| 173 |
+
# pytype static type analyzer
|
| 174 |
+
.pytype/
|
| 175 |
+
|
| 176 |
+
# Cython debug symbols
|
| 177 |
+
cython_debug/
|
| 178 |
+
|
| 179 |
+
# PyCharm
|
| 180 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 181 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 182 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 183 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 184 |
+
#.idea/
|
| 185 |
+
|
| 186 |
+
# Abstra
|
| 187 |
+
# Abstra is an AI-powered process automation framework.
|
| 188 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 189 |
+
# Learn more at https://abstra.io/docs
|
| 190 |
+
.abstra/
|
| 191 |
+
|
| 192 |
+
# Visual Studio Code
|
| 193 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 194 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 195 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 196 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 197 |
+
# .vscode/
|
| 198 |
+
|
| 199 |
+
# Ruff stuff:
|
| 200 |
+
.ruff_cache/
|
| 201 |
+
|
| 202 |
+
# PyPI configuration file
|
| 203 |
+
.pypirc
|
| 204 |
+
|
| 205 |
+
# Cursor
|
| 206 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 207 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 208 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 209 |
+
.cursorignore
|
| 210 |
+
.cursorindexingignore
|
| 211 |
+
|
| 212 |
+
# Marimo
|
| 213 |
+
marimo/_static/
|
| 214 |
+
marimo/_lsp/
|
| 215 |
+
__marimo__/
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
| 3 |
+
"python-envs.defaultPackageManager": "ms-python.python:conda"
|
| 4 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-bookworm
|
| 2 |
+
|
| 3 |
+
RUN apt-get update \
|
| 4 |
+
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
|
| 5 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
COPY . /app
|
| 9 |
+
|
| 10 |
+
ENV PORT=7860 \
|
| 11 |
+
DATABASE_URL=file:/app/prisma/dev.db \
|
| 12 |
+
EHR_BASE_URL=http://127.0.0.1:7860 \
|
| 13 |
+
PLAYWRIGHT_HEADLESS=true \
|
| 14 |
+
OPENENV_DEFAULT_WAIT_MS=350
|
| 15 |
+
|
| 16 |
+
RUN npm install \
|
| 17 |
+
&& python3 -m pip install --no-cache-dir . \
|
| 18 |
+
&& python3 -m playwright install --with-deps chromium \
|
| 19 |
+
&& npx prisma generate \
|
| 20 |
+
&& npx prisma db push \
|
| 21 |
+
&& npx prisma db seed \
|
| 22 |
+
&& npm run build:ehr
|
| 23 |
+
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
ENTRYPOINT ["./docker/entrypoint.sh"]
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EHRGym
|
| 2 |
+
|
| 3 |
+
**EHRGym** is a containerized environment for training and evaluating computer-use agents in an Epic-like electronic health record (EHR) workflow.
|
| 4 |
+
|
| 5 |
+
It combines:
|
| 6 |
+
|
| 7 |
+
- A web-based EHR built with **Next.js + TypeScript**
|
| 8 |
+
- An **OpenEnv-compliant environment server** built with **FastAPI + Playwright**
|
| 9 |
+
|
| 10 |
+
The environment exposes `reset()`, `step(action)`, and a `state` object so an agent can interact with the EHR through a real browser.
|
| 11 |
+
|
| 12 |
+
> **Note:** This project uses **synthetic data only** (no PHI).
|
| 13 |
+
> Not affiliated with or endorsed by Epic Systems.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Table of contents
|
| 18 |
+
|
| 19 |
+
- [Clinical focus (initial)](#clinical-focus-initial)
|
| 20 |
+
- [What you get](#what-you-get)
|
| 21 |
+
- [Goals](#goals)
|
| 22 |
+
- [Non-goals (initial)](#non-goals-initial)
|
| 23 |
+
- [Architecture (one environment instance)](#architecture-one-environment-instance)
|
| 24 |
+
- [EHR UI layout (Epic-like)](#ehr-ui-layout-epic-like)
|
| 25 |
+
- [OpenEnv interface](#openenv-interface)
|
| 26 |
+
- [Tasks (provider-focused)](#tasks-provider-focused)
|
| 27 |
+
- [Synthetic patients](#synthetic-patients)
|
| 28 |
+
- [Performance & training approach](#performance--training-approach)
|
| 29 |
+
- [Logging & evaluation](#logging--evaluation)
|
| 30 |
+
- [Repository layout (proposed)](#repository-layout-proposed)
|
| 31 |
+
- [Quickstart (placeholder)](#quickstart-placeholder)
|
| 32 |
+
- [Contributing](#contributing)
|
| 33 |
+
- [License](#license)
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## Clinical focus (initial)
|
| 38 |
+
|
| 39 |
+
Provider workflows:
|
| 40 |
+
|
| 41 |
+
- Reviewing the chart (encounters, labs, prior notes)
|
| 42 |
+
- Writing progress and encounter notes
|
| 43 |
+
- Placing and signing orders
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## What you get
|
| 48 |
+
|
| 49 |
+
- **Epic-like charting UI**
|
| 50 |
+
- Chart Review (Encounters / Labs / Clinical Notes)
|
| 51 |
+
- Notes authoring
|
| 52 |
+
- Orders with signing workflow
|
| 53 |
+
- Encounter sign/close
|
| 54 |
+
|
| 55 |
+
- **OpenEnv-compliant RL environment**
|
| 56 |
+
- Typed `Action`, `Observation`, `State`
|
| 57 |
+
- `reset()` / `step()` / `state()`
|
| 58 |
+
- Real browser interaction (Playwright)
|
| 59 |
+
|
| 60 |
+
- **Task library**
|
| 61 |
+
- Chart review → note → orders → sign/close
|
| 62 |
+
|
| 63 |
+
- **Synthetic patient pipeline**
|
| 64 |
+
- Baseline: **Synthea + FHIR-shaped ingest**
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## Goals
|
| 69 |
+
|
| 70 |
+
- OpenEnv compliance with typed `Action` / `Observation` / `State` models
|
| 71 |
+
- Docker-first deployment and reproducible containers
|
| 72 |
+
- Next.js EHR interface supporting:
|
| 73 |
+
- chart review (encounters, labs, clinical notes)
|
| 74 |
+
- order entry (labs / meds / imaging) with sign workflow
|
| 75 |
+
- note authoring (progress & encounter notes)
|
| 76 |
+
- Task-based RL episodes (patient + scenario + objective + scoring rubric)
|
| 77 |
+
- Synthetic patients only (no PHI), with realistic longitudinal timelines and standard coding where feasible
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## Out-of-Scope
|
| 82 |
+
|
| 83 |
+
- Pixel-perfect Epic cloning (We emulate workflows & info layout)
|
| 84 |
+
- Full enterprise EHR scope on day one (MAR, billing, scheduling, in-basket, prior auth, etc.)
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## Architecture
|
| 89 |
+
|
| 90 |
+
A single container runs two processes:
|
| 91 |
+
|
| 92 |
+
1. **Next.js EHR app (port 3000)**
|
| 93 |
+
- Serves the UI and required API routes (patient data, notes, orders, signing)
|
| 94 |
+
2. **OpenEnv environment server (port 8000)**
|
| 95 |
+
- FastAPI server exposing OpenEnv API
|
| 96 |
+
- Launches and controls headless Chromium via Playwright
|
| 97 |
+
- Implements `reset()`, `step()`, `state`, scenario sampling, and reward computation
|
| 98 |
+
|
| 99 |
+
**Data layer**
|
| 100 |
+
- SQLite via Prisma (portable and fast)
|
| 101 |
+
- On `reset()`, the environment recreates/truncates the DB and reseeds patients, encounters, labs, notes, orders, and scenario ground truth. Optionally use a DB snapshot + copy-on-reset for speed.
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## EHR UI layout (Epic-like)
|
| 106 |
+
|
| 107 |
+
- **Entry view:** patient list / schedule-like page → select patient → open chart
|
| 108 |
+
- **Chart shell**
|
| 109 |
+
- Activity sidebar: Summary, Chart Review, Orders, Notes (optional), Encounter (close/sign)
|
| 110 |
+
- Patient banner: synthetic demographics and key flags (synthetic ID, age/sex, allergies)
|
| 111 |
+
- **Chart Review tabs**
|
| 112 |
+
- Encounters: timeline, encounter detail, linked notes/orders
|
| 113 |
+
- Labs: table + trend view, filtering, abnormal flags
|
| 114 |
+
- Clinical Notes: list by type/date/author, open note
|
| 115 |
+
- **Notes**
|
| 116 |
+
- Create Progress Note tied to current encounter
|
| 117 |
+
- Structured sections (SOAP)
|
| 118 |
+
- Problem-oriented A/P that links naturally to orders
|
| 119 |
+
- **Orders**
|
| 120 |
+
- Search/select from constrained preference list
|
| 121 |
+
- Configure parameters (dose/frequency, lab timing)
|
| 122 |
+
- Statuses: Draft → Pending Signature → Signed
|
| 123 |
+
|
| 124 |
+
**RL instrumentation**
|
| 125 |
+
- Stable selectors (`data-testid` / `data-qa`) for tabs, lab rows, order rows, note controls
|
| 126 |
+
- Accessible labels (`aria-label`) so agents can use the accessibility tree
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## OpenEnv Interface
|
| 131 |
+
|
| 132 |
+
**Actions**
|
| 133 |
+
- Low-level computer-use actions (mouse clicks, drag, scroll, keypress, type, wait)
|
| 134 |
+
- Optional high-level actions for curriculum/debug (e.g., `click(selector)`, `fill(selector,text)`, `goto(path)`, `select_patient(patient_id)`)
|
| 135 |
+
|
| 136 |
+
**Observations**
|
| 137 |
+
- Goal/instruction text
|
| 138 |
+
- Downscaled screenshot (base64 PNG)
|
| 139 |
+
- Current route/URL and active activity context
|
| 140 |
+
- Optional DOM snapshot and/or accessibility tree
|
| 141 |
+
- Metadata (timing, action success, structured errors)
|
| 142 |
+
|
| 143 |
+
**State**
|
| 144 |
+
- `episode_id`, `step_count` + environment fields:
|
| 145 |
+
- `patient_id`, `encounter_id`, `scenario_id`
|
| 146 |
+
- `rubric_progress`
|
| 147 |
+
- `cumulative_reward`
|
| 148 |
+
|
| 149 |
+
**Rewarding**
|
| 150 |
+
- Terminal success when objective is satisfied (e.g., correct note signed + correct orders signed)
|
| 151 |
+
- Shaping rewards for meaningful substeps (navigate, find target lab, place required order, sign)
|
| 152 |
+
- Penalties for invalid actions, navigation errors, unsafe/irrelevant orders, excessive steps
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## Tasks
|
| 157 |
+
|
| 158 |
+
Scenarios are packaged as specs and optionally generated at reset. Example task families:
|
| 159 |
+
|
| 160 |
+
- **Chart Review → Labs**
|
| 161 |
+
- Find most recent creatinine; evaluate AKI criteria
|
| 162 |
+
- Trend hemoglobin over last 3 values; document in progress note
|
| 163 |
+
- **Chart Review → Encounters**
|
| 164 |
+
- Locate discharge summary; extract follow-up plan
|
| 165 |
+
- Identify prior antibiotic exposure from previous encounter orders
|
| 166 |
+
- **Clinical Notes**
|
| 167 |
+
- Open most recent consult; summarize recommendations
|
| 168 |
+
- **Progress note authoring**
|
| 169 |
+
- Complete SOAP note with required elements and grounded facts
|
| 170 |
+
- **Orders**
|
| 171 |
+
- Place specific orders with correct parameters; sign
|
| 172 |
+
- **Close/finish encounter**
|
| 173 |
+
- Signed note + signed orders + required fields
|
| 174 |
+
|
| 175 |
+
**Curriculum**
|
| 176 |
+
- Phase 0: unit skills (navigate, open tabs, filter labs, open note)
|
| 177 |
+
- Phase 1: single objective (place one order, sign one note)
|
| 178 |
+
- Phase 2: multi-step (review → note → orders → sign/close)
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
## Synthetic patients
|
| 183 |
+
|
| 184 |
+
Baseline approach:
|
| 185 |
+
|
| 186 |
+
- Use Synthea to generate longitudinal synthetic records (encounters, conditions, meds, labs/vitals, procedures, etc.), exportable as FHIR
|
| 187 |
+
- Treat FHIR R4 concepts as the internal “shape” even if stored relationally
|
| 188 |
+
- Use standard coding when feasible:
|
| 189 |
+
- LOINC for labs
|
| 190 |
+
- SNOMED CT for problems/findings/procedures
|
| 191 |
+
- RxNorm for meds
|
| 192 |
+
|
| 193 |
+
**Notes gap (free-text)**
|
| 194 |
+
- Template-based notes from structured facts (easy to score, less diverse)
|
| 195 |
+
- Constrained LLM-generated notes grounded strictly in chart facts (more realistic, needs guardrails)
|
| 196 |
+
- Hybrid: deterministic skeleton + constrained paraphrase
|
| 197 |
+
|
| 198 |
+
**Scenarios** layer on top of base patients as teaching cases (e.g., DKA, CHF, pneumonia, AKI, GI bleed) with explicit ground truth objectives:
|
| 199 |
+
- required orders
|
| 200 |
+
- required note elements
|
| 201 |
+
- critical facts that must appear in the note
|
| 202 |
+
|
| 203 |
+
---
|
| 204 |
+
|
| 205 |
+
## Performance and Training Approach
|
| 206 |
+
|
| 207 |
+
- Browser simulation throughput is usually the bottleneck, not GPU
|
| 208 |
+
- Start with demonstrations (scripted Playwright expert) → supervised behavioral cloning
|
| 209 |
+
- Move to RL after BC reliably solves simpler tasks
|
| 210 |
+
- Run a modest number of env containers concurrently (e.g., 4–16)
|
| 211 |
+
- Keep observations efficient (downscale screenshots; optionally omit DOM/a11y on “easy mode”)
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## Logging and Evaluation
|
| 216 |
+
|
| 217 |
+
**Logging per step**
|
| 218 |
+
- Action, success/failure, reward components, UI errors
|
| 219 |
+
|
| 220 |
+
**Episode artifacts**
|
| 221 |
+
- Final note text
|
| 222 |
+
- Orders placed/signed
|
| 223 |
+
- Optional screenshots for debugging
|
| 224 |
+
|
| 225 |
+
**Evaluation**
|
| 226 |
+
- Deterministic test suites with fixed seeds
|
| 227 |
+
- Metrics: task success rate, steps-to-completion, unsafe/irrelevant order rate, note completeness/grounding
|
| 228 |
+
|
| 229 |
+
**Safety**
|
| 230 |
+
- Synthetic data only (no PHI)
|
| 231 |
+
- Constrained formulary and order catalog
|
| 232 |
+
- If LLM-generated notes are used, enforce grounding checks (facts must be supported by chart)
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## Repository layout
|
| 237 |
+
|
| 238 |
+
```
|
| 239 |
+
apps/ehr/ Next.js EHR UI (TypeScript)
|
| 240 |
+
env_server/ FastAPI OpenEnv server + Playwright control
|
| 241 |
+
tasks/ scenario specs, rubrics, fixtures
|
| 242 |
+
synthetic/ Synthea generation + FHIR ingest + seed tooling
|
| 243 |
+
prisma/ schema + migrations
|
| 244 |
+
docker/ Dockerfiles + entrypoints
|
| 245 |
+
shared/ synthetic seed definitions + reset helpers
|
| 246 |
+
scripts/ example agent loop and local helpers
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
---
|
| 250 |
+
|
| 251 |
+
## Quickstart
|
| 252 |
+
|
| 253 |
+
The initial scaffold is now wired end-to-end.
|
| 254 |
+
|
| 255 |
+
### What is included
|
| 256 |
+
|
| 257 |
+
- **Next.js EHR UI** in [apps/ehr](apps/ehr)
|
| 258 |
+
- patient list / chart entry
|
| 259 |
+
- chart review with encounters, labs, notes
|
| 260 |
+
- progress note authoring
|
| 261 |
+
- order drafting and signing
|
| 262 |
+
- encounter sign workflow
|
| 263 |
+
- **FastAPI environment server** in [env_server](env_server)
|
| 264 |
+
- `POST /reset`
|
| 265 |
+
- `POST /step`
|
| 266 |
+
- `GET /state`
|
| 267 |
+
- `GET /healthz`
|
| 268 |
+
- **Prisma + SQLite** schema and seed data in [prisma](prisma) and [shared](shared)
|
| 269 |
+
- **Docker** single-container startup files in [docker](docker) and [docker-compose.yml](docker-compose.yml)
|
| 270 |
+
|
| 271 |
+
### Local development
|
| 272 |
+
|
| 273 |
+
Prerequisites:
|
| 274 |
+
|
| 275 |
+
- Node.js 20+
|
| 276 |
+
- Python 3.9+
|
| 277 |
+
|
| 278 |
+
1. Install Node dependencies:
|
| 279 |
+
|
| 280 |
+
`npm install`
|
| 281 |
+
|
| 282 |
+
2. Install the Python environment server package:
|
| 283 |
+
|
| 284 |
+
`python3 -m pip install .`
|
| 285 |
+
|
| 286 |
+
If you use a virtual environment or conda environment, activate it before running the remaining commands.
|
| 287 |
+
|
| 288 |
+
3. Install the browser runtime for Playwright:
|
| 289 |
+
|
| 290 |
+
`python3 -m playwright install chromium`
|
| 291 |
+
|
| 292 |
+
4. Copy environment variables if needed:
|
| 293 |
+
|
| 294 |
+
`cp .env.example .env`
|
| 295 |
+
|
| 296 |
+
5. Initialize the SQLite database:
|
| 297 |
+
|
| 298 |
+
`npx prisma generate && npx prisma db push && npx prisma db seed`
|
| 299 |
+
|
| 300 |
+
6. Start both processes:
|
| 301 |
+
|
| 302 |
+
`npm run dev`
|
| 303 |
+
|
| 304 |
+
Available endpoints:
|
| 305 |
+
|
| 306 |
+
- EHR UI: http://127.0.0.1:3000
|
| 307 |
+
- Env server: http://127.0.0.1:8000
|
| 308 |
+
|
| 309 |
+
### Docker
|
| 310 |
+
|
| 311 |
+
Build and run the combined container:
|
| 312 |
+
|
| 313 |
+
`docker compose up --build`
|
| 314 |
+
|
| 315 |
+
This launches:
|
| 316 |
+
|
| 317 |
+
- the Next.js EHR app on port `3000`
|
| 318 |
+
- the FastAPI environment server on port `8000`
|
| 319 |
+
|
| 320 |
+
### Minimal API flow
|
| 321 |
+
|
| 322 |
+
1. `POST /reset`
|
| 323 |
+
2. Read `observation` and `state`
|
| 324 |
+
3. Send browser-style actions to `POST /step`
|
| 325 |
+
4. Inspect `GET /state` for episode progress
|
| 326 |
+
|
| 327 |
+
A starter agent loop is included in [scripts/example_agent.py](scripts/example_agent.py).
|
| 328 |
+
|
| 329 |
+
---
|
| 330 |
+
|
| 331 |
+
## Contributing
|
| 332 |
+
|
| 333 |
+
- Keep all data synthetic
|
| 334 |
+
- Add `data-testid` / `aria-label` for any new interactive UI element
|
| 335 |
+
- New tasks should include:
|
| 336 |
+
- objective text
|
| 337 |
+
- ground truth artifacts (required orders/note fields)
|
| 338 |
+
- rubric scoring rules
|
| 339 |
+
- deterministic seed behavior
|
| 340 |
+
|
| 341 |
+
---
|
| 342 |
+
|
| 343 |
+
## License
|
| 344 |
+
|
| 345 |
+
Apache License
|
| 346 |
+
Version 2.0, January 2004
|
| 347 |
+
|
| 348 |
+
This project is licensed under the Apache License, Version 2.0.
|
| 349 |
+
You should include the full license text in a file named `LICENSE` at the repository root.
|
| 350 |
+
|
| 351 |
+
Copyright [2026] [Adrian Serapio]
|
| 352 |
+
|
| 353 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 354 |
+
you may not use this file except in compliance with the License.
|
| 355 |
+
You may obtain a copy of the License at
|
| 356 |
+
|
| 357 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
apps/ehr/app/api/dev/reset/route.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
import { prisma } from "../../../../lib/db";
|
| 4 |
+
import { resetDatabase } from "../../../../../../shared/reset-database";
|
| 5 |
+
|
| 6 |
+
export async function POST() {
|
| 7 |
+
await resetDatabase(prisma);
|
| 8 |
+
|
| 9 |
+
const patientCount = await prisma.patient.count();
|
| 10 |
+
|
| 11 |
+
return NextResponse.json({
|
| 12 |
+
ok: true,
|
| 13 |
+
patientCount
|
| 14 |
+
});
|
| 15 |
+
}
|
apps/ehr/app/api/patients/[id]/route.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
import { parseJsonValue } from "../../../../lib/chart";
|
| 4 |
+
import { prisma } from "../../../../lib/db";
|
| 5 |
+
|
| 6 |
+
type ApiPatient = {
|
| 7 |
+
id: string;
|
| 8 |
+
mrn: string;
|
| 9 |
+
fullName: string;
|
| 10 |
+
age: number;
|
| 11 |
+
sex: string;
|
| 12 |
+
allergiesJson: string;
|
| 13 |
+
bannerFlagsJson: string;
|
| 14 |
+
summary: string;
|
| 15 |
+
encounters: Array<{
|
| 16 |
+
id: string;
|
| 17 |
+
type: string;
|
| 18 |
+
reasonForVisit: string;
|
| 19 |
+
provider: string;
|
| 20 |
+
startedAt: Date;
|
| 21 |
+
status: string;
|
| 22 |
+
labs: Array<{
|
| 23 |
+
id: string;
|
| 24 |
+
name: string;
|
| 25 |
+
loinc: string | null;
|
| 26 |
+
value: string;
|
| 27 |
+
unit: string;
|
| 28 |
+
referenceRange: string;
|
| 29 |
+
abnormal: boolean;
|
| 30 |
+
collectedAt: Date;
|
| 31 |
+
}>;
|
| 32 |
+
notes: Array<{
|
| 33 |
+
id: string;
|
| 34 |
+
type: string;
|
| 35 |
+
title: string;
|
| 36 |
+
author: string;
|
| 37 |
+
content: string;
|
| 38 |
+
signed: boolean;
|
| 39 |
+
createdAt: Date;
|
| 40 |
+
}>;
|
| 41 |
+
orders: Array<{
|
| 42 |
+
id: string;
|
| 43 |
+
name: string;
|
| 44 |
+
category: string;
|
| 45 |
+
parametersJson: string;
|
| 46 |
+
status: string;
|
| 47 |
+
rationale: string;
|
| 48 |
+
createdAt: Date;
|
| 49 |
+
}>;
|
| 50 |
+
}>;
|
| 51 |
+
scenarios: Array<{
|
| 52 |
+
id: string;
|
| 53 |
+
encounterId: string;
|
| 54 |
+
title: string;
|
| 55 |
+
objective: string;
|
| 56 |
+
rubricJson: string;
|
| 57 |
+
requiredOrdersJson: string;
|
| 58 |
+
requiredNoteElementsJson: string;
|
| 59 |
+
}>;
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
export async function GET(_: Request, context: { params: Promise<{ id: string }> }) {
|
| 63 |
+
const { id } = await context.params;
|
| 64 |
+
|
| 65 |
+
const patient: ApiPatient | null = await prisma.patient.findUnique({
|
| 66 |
+
where: { id },
|
| 67 |
+
include: {
|
| 68 |
+
encounters: {
|
| 69 |
+
orderBy: { startedAt: "desc" },
|
| 70 |
+
include: {
|
| 71 |
+
labs: { orderBy: { collectedAt: "desc" } },
|
| 72 |
+
notes: { orderBy: { createdAt: "desc" } },
|
| 73 |
+
orders: { orderBy: { createdAt: "desc" } }
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
scenarios: {
|
| 77 |
+
orderBy: { createdAt: "desc" }
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
if (!patient) {
|
| 83 |
+
return NextResponse.json({ error: "Patient not found" }, { status: 404 });
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return NextResponse.json({
|
| 87 |
+
patient: {
|
| 88 |
+
id: patient.id,
|
| 89 |
+
mrn: patient.mrn,
|
| 90 |
+
fullName: patient.fullName,
|
| 91 |
+
age: patient.age,
|
| 92 |
+
sex: patient.sex,
|
| 93 |
+
allergies: parseJsonValue<string[]>(patient.allergiesJson),
|
| 94 |
+
bannerFlags: parseJsonValue<string[]>(patient.bannerFlagsJson),
|
| 95 |
+
summary: patient.summary,
|
| 96 |
+
encounters: patient.encounters.map((encounter: ApiPatient["encounters"][number]) => ({
|
| 97 |
+
...encounter,
|
| 98 |
+
orders: encounter.orders.map((order: ApiPatient["encounters"][number]["orders"][number]) => ({
|
| 99 |
+
...order,
|
| 100 |
+
parameters: parseJsonValue<Record<string, string>>(order.parametersJson)
|
| 101 |
+
}))
|
| 102 |
+
})),
|
| 103 |
+
scenarios: patient.scenarios.map((scenario: ApiPatient["scenarios"][number]) => ({
|
| 104 |
+
...scenario,
|
| 105 |
+
rubric: parseJsonValue<string[]>(scenario.rubricJson),
|
| 106 |
+
requiredOrders: parseJsonValue<string[]>(scenario.requiredOrdersJson),
|
| 107 |
+
requiredNoteElements: parseJsonValue<string[]>(scenario.requiredNoteElementsJson)
|
| 108 |
+
}))
|
| 109 |
+
}
|
| 110 |
+
});
|
| 111 |
+
}
|
apps/ehr/app/api/patients/route.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
import { parseJsonValue } from "../../../lib/chart";
|
| 4 |
+
import { prisma } from "../../../lib/db";
|
| 5 |
+
|
| 6 |
+
export async function GET() {
|
| 7 |
+
const patients = await prisma.patient.findMany({
|
| 8 |
+
orderBy: { fullName: "asc" },
|
| 9 |
+
include: {
|
| 10 |
+
encounters: {
|
| 11 |
+
take: 1,
|
| 12 |
+
orderBy: { startedAt: "desc" }
|
| 13 |
+
},
|
| 14 |
+
scenarios: {
|
| 15 |
+
take: 1,
|
| 16 |
+
orderBy: { createdAt: "desc" }
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
return NextResponse.json({
|
| 22 |
+
patients: patients.map((patient) => ({
|
| 23 |
+
id: patient.id,
|
| 24 |
+
mrn: patient.mrn,
|
| 25 |
+
fullName: patient.fullName,
|
| 26 |
+
age: patient.age,
|
| 27 |
+
sex: patient.sex,
|
| 28 |
+
allergies: parseJsonValue<string[]>(patient.allergiesJson),
|
| 29 |
+
bannerFlags: parseJsonValue<string[]>(patient.bannerFlagsJson),
|
| 30 |
+
summary: patient.summary,
|
| 31 |
+
encounter: patient.encounters[0] ?? null,
|
| 32 |
+
scenario: patient.scenarios[0] ?? null
|
| 33 |
+
}))
|
| 34 |
+
});
|
| 35 |
+
}
|
apps/ehr/app/globals.css
ADDED
|
@@ -0,0 +1,1646 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: light;
|
| 3 |
+
font-family: var(--font-body), "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
|
| 4 |
+
background: #ffffff;
|
| 5 |
+
color: #000000;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
* {
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
html,
|
| 13 |
+
body {
|
| 14 |
+
margin: 0;
|
| 15 |
+
min-height: 100%;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
body {
|
| 19 |
+
background: #ffffff;
|
| 20 |
+
font-family: var(--font-body), "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
|
| 21 |
+
color: #000000;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
h1,
|
| 25 |
+
h2,
|
| 26 |
+
h3,
|
| 27 |
+
h4,
|
| 28 |
+
h5,
|
| 29 |
+
h6,
|
| 30 |
+
button,
|
| 31 |
+
input,
|
| 32 |
+
textarea,
|
| 33 |
+
select,
|
| 34 |
+
label,
|
| 35 |
+
nav,
|
| 36 |
+
.app-brand,
|
| 37 |
+
.badge,
|
| 38 |
+
.status-pill,
|
| 39 |
+
.summary-flag,
|
| 40 |
+
.workspace-sidebar,
|
| 41 |
+
.workspace-header,
|
| 42 |
+
.workspace-hero,
|
| 43 |
+
.patient-hero,
|
| 44 |
+
.workspace-toggle,
|
| 45 |
+
.workspace-backlink,
|
| 46 |
+
.workspace-icon-button,
|
| 47 |
+
.workspace-profile,
|
| 48 |
+
.table th,
|
| 49 |
+
.ehr-table__header,
|
| 50 |
+
.problem-list__status,
|
| 51 |
+
.patient-hero__eyebrow {
|
| 52 |
+
font-family: var(--font-ui), "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
a {
|
| 56 |
+
color: inherit;
|
| 57 |
+
text-decoration: none;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
button,
|
| 61 |
+
input,
|
| 62 |
+
textarea,
|
| 63 |
+
select {
|
| 64 |
+
font: inherit;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
button {
|
| 68 |
+
cursor: pointer;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.epic-shell {
|
| 72 |
+
min-height: 100vh;
|
| 73 |
+
max-width: 1600px;
|
| 74 |
+
margin: 0 auto;
|
| 75 |
+
padding: 18px 20px 28px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.epic-topbar {
|
| 79 |
+
display: grid;
|
| 80 |
+
grid-template-columns: auto 1fr auto;
|
| 81 |
+
gap: 20px;
|
| 82 |
+
align-items: center;
|
| 83 |
+
padding: 4px 0 18px;
|
| 84 |
+
border: none;
|
| 85 |
+
border-bottom: 1px solid #e5e7eb;
|
| 86 |
+
border-radius: 0;
|
| 87 |
+
background: transparent;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.epic-topbar__brand,
|
| 91 |
+
.epic-topbar__meta,
|
| 92 |
+
.section-card h2,
|
| 93 |
+
.section-card h3 {
|
| 94 |
+
display: flex;
|
| 95 |
+
align-items: center;
|
| 96 |
+
gap: 10px;
|
| 97 |
+
margin: 0;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.app-brand {
|
| 101 |
+
display: inline-flex;
|
| 102 |
+
align-items: center;
|
| 103 |
+
gap: 14px;
|
| 104 |
+
min-width: 0;
|
| 105 |
+
text-decoration: none;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.app-brand__logo {
|
| 109 |
+
width: 48px;
|
| 110 |
+
height: 48px;
|
| 111 |
+
display: inline-flex;
|
| 112 |
+
align-items: center;
|
| 113 |
+
justify-content: center;
|
| 114 |
+
flex: 0 0 auto;
|
| 115 |
+
user-select: none;
|
| 116 |
+
-webkit-user-select: none;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.app-brand__logo--compact {
|
| 120 |
+
width: 42px;
|
| 121 |
+
height: 42px;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.app-brand__logo img {
|
| 125 |
+
width: 100%;
|
| 126 |
+
height: 100%;
|
| 127 |
+
object-fit: contain;
|
| 128 |
+
user-select: none;
|
| 129 |
+
-webkit-user-drag: none;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.app-brand__copy {
|
| 133 |
+
display: grid;
|
| 134 |
+
gap: 2px;
|
| 135 |
+
min-width: 0;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.app-brand__copy--compact {
|
| 139 |
+
gap: 1px;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.app-brand__copy strong {
|
| 143 |
+
font-size: 1.05rem;
|
| 144 |
+
line-height: 1.1;
|
| 145 |
+
color: #000000;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.app-brand__copy span {
|
| 149 |
+
font-size: 0.86rem;
|
| 150 |
+
color: #000000;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.epic-topbar__brand p,
|
| 154 |
+
.epic-topbar__meta,
|
| 155 |
+
.section-card p,
|
| 156 |
+
.muted {
|
| 157 |
+
color: #000000;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.epic-topbar__tabs {
|
| 161 |
+
display: flex;
|
| 162 |
+
gap: 8px;
|
| 163 |
+
flex-wrap: wrap;
|
| 164 |
+
justify-content: center;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.nav-link {
|
| 168 |
+
text-decoration: none;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.epic-tab,
|
| 172 |
+
.epic-topbar__tabs .nav-link {
|
| 173 |
+
padding: 10px 14px;
|
| 174 |
+
border: 1px solid #e5e7eb;
|
| 175 |
+
border-radius: 999px;
|
| 176 |
+
background: #ffffff;
|
| 177 |
+
font-size: 0.9rem;
|
| 178 |
+
font-weight: 600;
|
| 179 |
+
color: #4b5563;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.epic-tab--active,
|
| 183 |
+
.epic-topbar__tabs .nav-link.is-active {
|
| 184 |
+
background: #eef2ff;
|
| 185 |
+
border-color: #c7d2fe;
|
| 186 |
+
color: #4338ca;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.epic-contextbar,
|
| 190 |
+
.chart-activity-bar {
|
| 191 |
+
display: flex;
|
| 192 |
+
justify-content: space-between;
|
| 193 |
+
align-items: center;
|
| 194 |
+
gap: 12px;
|
| 195 |
+
padding: 14px 20px;
|
| 196 |
+
border: 1px solid #e5e7eb;
|
| 197 |
+
background: #ffffff;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.epic-contextbar {
|
| 201 |
+
margin-bottom: 16px;
|
| 202 |
+
border-radius: 18px;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.epic-contextbar p {
|
| 206 |
+
margin: 2px 0 0;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.epic-contextbar__actions {
|
| 210 |
+
display: flex;
|
| 211 |
+
gap: 8px;
|
| 212 |
+
flex-wrap: wrap;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.chart-activity-bar {
|
| 216 |
+
justify-content: flex-start;
|
| 217 |
+
flex-wrap: wrap;
|
| 218 |
+
margin-bottom: 16px;
|
| 219 |
+
border-radius: 18px;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.chart-activity-bar a {
|
| 223 |
+
padding: 9px 14px;
|
| 224 |
+
border: 1px solid #e5e7eb;
|
| 225 |
+
border-radius: 999px;
|
| 226 |
+
background: #f9fafb;
|
| 227 |
+
font-size: 0.9rem;
|
| 228 |
+
font-weight: 600;
|
| 229 |
+
color: #4b5563;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.chart-activity-bar a.is-active,
|
| 233 |
+
.sidebar-nav a.is-active {
|
| 234 |
+
background: #eef2ff;
|
| 235 |
+
border-color: #c7d2fe;
|
| 236 |
+
color: #4338ca;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.grid {
|
| 240 |
+
display: grid;
|
| 241 |
+
gap: 14px;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.grid--2 {
|
| 245 |
+
grid-template-columns: 1.2fr 0.8fr;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.grid--3 {
|
| 249 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.workspace-grid {
|
| 253 |
+
display: grid;
|
| 254 |
+
gap: 16px;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.workspace-grid--home {
|
| 258 |
+
grid-template-columns: 260px minmax(0, 1fr) 320px;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.workspace-grid--chart {
|
| 262 |
+
grid-template-columns: 250px minmax(0, 1fr) 300px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.center-pane,
|
| 266 |
+
.right-rail,
|
| 267 |
+
.left-rail,
|
| 268 |
+
.patient-list,
|
| 269 |
+
.sidebar-nav,
|
| 270 |
+
.info-chips,
|
| 271 |
+
.timeline,
|
| 272 |
+
.data-list,
|
| 273 |
+
.order-list,
|
| 274 |
+
.note-list,
|
| 275 |
+
.rail-list,
|
| 276 |
+
.problem-list {
|
| 277 |
+
display: grid;
|
| 278 |
+
gap: 12px;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.patient-card,
|
| 282 |
+
.sidebar-nav a,
|
| 283 |
+
.info-chip,
|
| 284 |
+
.list-row,
|
| 285 |
+
.order-row,
|
| 286 |
+
.note-row,
|
| 287 |
+
.lab-row,
|
| 288 |
+
.section-card {
|
| 289 |
+
background: #ffffff;
|
| 290 |
+
border: 1px solid #e5e7eb;
|
| 291 |
+
border-radius: 18px;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.patient-card,
|
| 295 |
+
.list-row,
|
| 296 |
+
.order-row,
|
| 297 |
+
.note-row,
|
| 298 |
+
.lab-row,
|
| 299 |
+
.section-card {
|
| 300 |
+
padding: 16px;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.patient-card:hover,
|
| 304 |
+
.sidebar-nav a:hover {
|
| 305 |
+
border-color: #d1d5db;
|
| 306 |
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.patient-card header,
|
| 310 |
+
.list-row header,
|
| 311 |
+
.order-row header,
|
| 312 |
+
.note-row header,
|
| 313 |
+
.section-card__header,
|
| 314 |
+
.banner,
|
| 315 |
+
.patient-header,
|
| 316 |
+
.patient-header__identity,
|
| 317 |
+
.patient-header__facts {
|
| 318 |
+
display: flex;
|
| 319 |
+
justify-content: space-between;
|
| 320 |
+
gap: 12px;
|
| 321 |
+
align-items: flex-start;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.badge,
|
| 325 |
+
.status-pill {
|
| 326 |
+
display: inline-flex;
|
| 327 |
+
align-items: center;
|
| 328 |
+
gap: 6px;
|
| 329 |
+
padding: 6px 10px;
|
| 330 |
+
border-radius: 999px;
|
| 331 |
+
font-size: 0.78rem;
|
| 332 |
+
font-weight: 700;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.badge {
|
| 336 |
+
background: #f9fafb;
|
| 337 |
+
color: #4b5563;
|
| 338 |
+
border: 1px solid #e5e7eb;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.status-pill[data-status="OPEN"],
|
| 342 |
+
.status-pill[data-status="DRAFT"] {
|
| 343 |
+
background: #fff7ed;
|
| 344 |
+
color: #c2410c;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.status-pill[data-status="PENDING_SIGNATURE"] {
|
| 348 |
+
background: #eff6ff;
|
| 349 |
+
color: #2563eb;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.status-pill[data-status="SIGNED"],
|
| 353 |
+
.status-pill[data-status="CLOSED"] {
|
| 354 |
+
background: #ecfdf5;
|
| 355 |
+
color: #059669;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.info-chip {
|
| 359 |
+
padding: 10px 12px;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.sidebar-nav a {
|
| 363 |
+
padding: 12px 14px;
|
| 364 |
+
font-weight: 600;
|
| 365 |
+
background: #f9fafb;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.section-stack {
|
| 369 |
+
display: grid;
|
| 370 |
+
gap: 10px;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.section-card__header {
|
| 374 |
+
margin-bottom: 12px;
|
| 375 |
+
padding-bottom: 10px;
|
| 376 |
+
border-bottom: 1px solid #f1f5f9;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.section-card__header h2 {
|
| 380 |
+
font-size: 1rem;
|
| 381 |
+
color: #111827;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.section-card__header p {
|
| 385 |
+
margin: 4px 0 0;
|
| 386 |
+
font-size: 0.86rem;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
textarea,
|
| 390 |
+
input,
|
| 391 |
+
select {
|
| 392 |
+
border: 1px solid #e5e7eb;
|
| 393 |
+
border-radius: 12px;
|
| 394 |
+
padding: 10px 12px;
|
| 395 |
+
background: #ffffff;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
textarea,
|
| 399 |
+
input:not([type="checkbox"]):not([type="radio"]),
|
| 400 |
+
select {
|
| 401 |
+
width: 100%;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
input[type="checkbox"],
|
| 405 |
+
input[type="radio"] {
|
| 406 |
+
width: auto;
|
| 407 |
+
margin: 0;
|
| 408 |
+
accent-color: #35638c;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
textarea {
|
| 412 |
+
min-height: 140px;
|
| 413 |
+
resize: vertical;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.field {
|
| 417 |
+
display: grid;
|
| 418 |
+
gap: 6px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.field > span {
|
| 422 |
+
display: inline-block;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.form-grid {
|
| 426 |
+
display: grid;
|
| 427 |
+
gap: 10px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.form-actions {
|
| 431 |
+
display: flex;
|
| 432 |
+
align-items: center;
|
| 433 |
+
gap: 10px;
|
| 434 |
+
flex-wrap: wrap;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.checkbox-field {
|
| 438 |
+
display: inline-flex;
|
| 439 |
+
align-items: center;
|
| 440 |
+
gap: 8px;
|
| 441 |
+
width: fit-content;
|
| 442 |
+
padding: 6px 0;
|
| 443 |
+
color: #42586d;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.checkbox-field__control {
|
| 447 |
+
flex: 0 0 auto;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.form-grid--2 {
|
| 451 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.primary-button,
|
| 455 |
+
.secondary-button {
|
| 456 |
+
border: 1px solid #e5e7eb;
|
| 457 |
+
border-radius: 12px;
|
| 458 |
+
padding: 10px 14px;
|
| 459 |
+
font-weight: 700;
|
| 460 |
+
width: auto;
|
| 461 |
+
min-width: 144px;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.primary-button {
|
| 465 |
+
background: #4f46e5;
|
| 466 |
+
color: #ffffff;
|
| 467 |
+
border-color: #4f46e5;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.secondary-button {
|
| 471 |
+
background: #ffffff;
|
| 472 |
+
color: #111827;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.table {
|
| 476 |
+
width: 100%;
|
| 477 |
+
border-collapse: collapse;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.table th,
|
| 481 |
+
.table td {
|
| 482 |
+
padding: 10px 8px;
|
| 483 |
+
text-align: left;
|
| 484 |
+
border-bottom: 1px solid #d9e3ea;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.table th {
|
| 488 |
+
font-size: 0.82rem;
|
| 489 |
+
text-transform: uppercase;
|
| 490 |
+
letter-spacing: 0.04em;
|
| 491 |
+
color: #6b7280;
|
| 492 |
+
background: #f9fafb;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.table tr:last-child td {
|
| 496 |
+
border-bottom: none;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.abnormal {
|
| 500 |
+
color: #b22222;
|
| 501 |
+
font-weight: 700;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.toolbar {
|
| 505 |
+
display: flex;
|
| 506 |
+
justify-content: space-between;
|
| 507 |
+
gap: 12px;
|
| 508 |
+
align-items: center;
|
| 509 |
+
margin-top: 10px;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.patient-header {
|
| 513 |
+
margin-bottom: 16px;
|
| 514 |
+
padding: 20px;
|
| 515 |
+
border: 1px solid #e5e7eb;
|
| 516 |
+
border-radius: 22px;
|
| 517 |
+
background: #ffffff;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.patient-header__identity {
|
| 521 |
+
align-items: center;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.patient-header__identity h1 {
|
| 525 |
+
margin: 2px 0;
|
| 526 |
+
font-size: 1.45rem;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.patient-header__identity p,
|
| 530 |
+
.patient-header__meta {
|
| 531 |
+
margin: 0;
|
| 532 |
+
color: #6b7280;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
.patient-avatar,
|
| 536 |
+
.patient-summary-card__avatar {
|
| 537 |
+
width: 54px;
|
| 538 |
+
height: 54px;
|
| 539 |
+
border-radius: 50%;
|
| 540 |
+
background: #f3f4f6;
|
| 541 |
+
border: 1px solid #e5e7eb;
|
| 542 |
+
display: inline-flex;
|
| 543 |
+
align-items: center;
|
| 544 |
+
justify-content: center;
|
| 545 |
+
font-weight: 700;
|
| 546 |
+
color: #4b5563;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.patient-header__facts {
|
| 550 |
+
align-items: center;
|
| 551 |
+
flex-wrap: wrap;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.patient-fact {
|
| 555 |
+
min-width: 180px;
|
| 556 |
+
display: grid;
|
| 557 |
+
gap: 3px;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.patient-fact strong {
|
| 561 |
+
font-size: 0.78rem;
|
| 562 |
+
color: #9ca3af;
|
| 563 |
+
text-transform: uppercase;
|
| 564 |
+
letter-spacing: 0.04em;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.patient-fact--priority span {
|
| 568 |
+
color: #111827;
|
| 569 |
+
font-weight: 600;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.left-rail,
|
| 573 |
+
.right-rail {
|
| 574 |
+
align-content: start;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
.patient-summary-card,
|
| 578 |
+
.patient-sidebar-card {
|
| 579 |
+
padding: 18px;
|
| 580 |
+
border: 1px solid #e5e7eb;
|
| 581 |
+
border-radius: 18px;
|
| 582 |
+
background: #ffffff;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.patient-summary-card {
|
| 586 |
+
display: grid;
|
| 587 |
+
grid-template-columns: 54px 1fr;
|
| 588 |
+
gap: 12px;
|
| 589 |
+
align-items: center;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.patient-summary-card h2,
|
| 593 |
+
.patient-sidebar-card h2 {
|
| 594 |
+
margin: 0;
|
| 595 |
+
font-size: 1rem;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.patient-sidebar-card__label {
|
| 599 |
+
font-size: 0.75rem;
|
| 600 |
+
text-transform: uppercase;
|
| 601 |
+
letter-spacing: 0.08em;
|
| 602 |
+
color: #9ca3af;
|
| 603 |
+
margin-bottom: 6px;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.rail-list__item,
|
| 607 |
+
.summary-panel__row,
|
| 608 |
+
.problem-list__item {
|
| 609 |
+
display: flex;
|
| 610 |
+
justify-content: space-between;
|
| 611 |
+
gap: 10px;
|
| 612 |
+
align-items: flex-start;
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.rail-list__item span,
|
| 616 |
+
.summary-panel__row span,
|
| 617 |
+
.problem-list__item span:last-child {
|
| 618 |
+
color: #6b7280;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.ehr-table__header,
|
| 622 |
+
.patient-card--table {
|
| 623 |
+
display: grid;
|
| 624 |
+
grid-template-columns: 1.2fr 0.7fr 1.3fr 0.6fr 1fr;
|
| 625 |
+
gap: 12px;
|
| 626 |
+
align-items: start;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.ehr-table__header {
|
| 630 |
+
padding: 0 8px 12px;
|
| 631 |
+
border-bottom: 1px solid #f1f5f9;
|
| 632 |
+
font-size: 0.78rem;
|
| 633 |
+
font-weight: 700;
|
| 634 |
+
color: #9ca3af;
|
| 635 |
+
text-transform: uppercase;
|
| 636 |
+
letter-spacing: 0.04em;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
.patient-list--table {
|
| 640 |
+
margin-top: 10px;
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.patient-card--table {
|
| 644 |
+
color: inherit;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.patient-card--table p,
|
| 648 |
+
.patient-card--table strong {
|
| 649 |
+
margin: 0;
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
.section-card--rail,
|
| 653 |
+
.section-card--summary {
|
| 654 |
+
background: #ffffff;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
.section-card--worklist {
|
| 658 |
+
border-top: 1px solid #d8e0ea;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.section-card--chart {
|
| 662 |
+
border-top: 1px solid #d8e0ea;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
.section-card--notes {
|
| 666 |
+
border-top: 1px solid #d8e0ea;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
.section-card--orders {
|
| 670 |
+
border-top: 1px solid #d8e0ea;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.section-card--wrapup {
|
| 674 |
+
border-top: 1px solid #d8e0ea;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
.section-card--problem-list {
|
| 678 |
+
border-top: 1px solid #d8e0ea;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
.section-card--diagnosis-list {
|
| 682 |
+
border-top: 1px solid #d8e0ea;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.summary-panel,
|
| 686 |
+
.summary-flags {
|
| 687 |
+
display: grid;
|
| 688 |
+
gap: 8px;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.summary-flag {
|
| 692 |
+
display: inline-flex;
|
| 693 |
+
padding: 6px 10px;
|
| 694 |
+
border: 1px solid #e5e7eb;
|
| 695 |
+
border-radius: 999px;
|
| 696 |
+
background: #f9fafb;
|
| 697 |
+
color: #4b5563;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.summary-flag--accent {
|
| 701 |
+
background: #eef2ff;
|
| 702 |
+
border-color: #c7d2fe;
|
| 703 |
+
color: #4338ca;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
.subtab-strip {
|
| 707 |
+
display: flex;
|
| 708 |
+
gap: 8px;
|
| 709 |
+
flex-wrap: wrap;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
.subtab-strip__item {
|
| 713 |
+
appearance: none;
|
| 714 |
+
padding: 9px 14px;
|
| 715 |
+
border: 1px solid #e5e7eb;
|
| 716 |
+
border-radius: 999px;
|
| 717 |
+
background: #f9fafb;
|
| 718 |
+
font-size: 0.88rem;
|
| 719 |
+
color: #4b5563;
|
| 720 |
+
cursor: pointer;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
.subtab-strip__item--active {
|
| 724 |
+
background: #eef2ff;
|
| 725 |
+
border-color: #c7d2fe;
|
| 726 |
+
color: #4338ca;
|
| 727 |
+
font-weight: 700;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.problem-list__item {
|
| 731 |
+
padding: 8px 10px;
|
| 732 |
+
border: 1px solid #f1f5f9;
|
| 733 |
+
border-radius: 12px;
|
| 734 |
+
background: #ffffff;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.problem-list__status {
|
| 738 |
+
color: #4f46e5;
|
| 739 |
+
font-weight: 700;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
.problem-list__status--muted {
|
| 743 |
+
color: #6b7280;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
.list-row--compact {
|
| 747 |
+
gap: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.list-row--compact p {
|
| 751 |
+
margin: 4px 0 0;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.primary-button:focus-visible,
|
| 755 |
+
.secondary-button:focus-visible,
|
| 756 |
+
.subtab-strip__item:focus-visible,
|
| 757 |
+
.chart-activity-bar a:focus-visible,
|
| 758 |
+
.sidebar-nav a:focus-visible,
|
| 759 |
+
.patient-card:focus-visible,
|
| 760 |
+
input:focus-visible,
|
| 761 |
+
textarea:focus-visible,
|
| 762 |
+
select:focus-visible {
|
| 763 |
+
outline: 2px solid #2563eb;
|
| 764 |
+
outline-offset: 2px;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
@media (max-width: 1080px) {
|
| 768 |
+
.grid--2,
|
| 769 |
+
.form-grid--2,
|
| 770 |
+
.grid--3 {
|
| 771 |
+
grid-template-columns: 1fr;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
.workspace-grid--home,
|
| 775 |
+
.workspace-grid--chart,
|
| 776 |
+
.ehr-table__header,
|
| 777 |
+
.patient-card--table,
|
| 778 |
+
.patient-header,
|
| 779 |
+
.patient-header__facts,
|
| 780 |
+
.epic-topbar,
|
| 781 |
+
.epic-contextbar {
|
| 782 |
+
grid-template-columns: 1fr;
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
align-items: flex-start;
|
| 785 |
+
}
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
.dashboard-shell {
|
| 789 |
+
min-height: 100vh;
|
| 790 |
+
display: grid;
|
| 791 |
+
grid-template-columns: 280px minmax(0, 1fr);
|
| 792 |
+
gap: 24px;
|
| 793 |
+
padding: 20px;
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
.dashboard-main {
|
| 797 |
+
display: grid;
|
| 798 |
+
gap: 18px;
|
| 799 |
+
align-content: start;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.workspace-sidebar {
|
| 803 |
+
gap: 16px;
|
| 804 |
+
position: sticky;
|
| 805 |
+
top: 20px;
|
| 806 |
+
align-self: start;
|
| 807 |
+
min-height: calc(100vh - 40px);
|
| 808 |
+
display: grid;
|
| 809 |
+
grid-template-rows: auto 1fr auto;
|
| 810 |
+
gap: 24px;
|
| 811 |
+
padding: 18px;
|
| 812 |
+
border: 1px solid #e5e7eb;
|
| 813 |
+
border-radius: 28px;
|
| 814 |
+
background: rgba(255, 255, 255, 0.92);
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.workspace-sidebar__brand {
|
| 818 |
+
padding-bottom: 8px;
|
| 819 |
+
padding-bottom: 2px;
|
| 820 |
+
border-bottom: 1px solid #eef2f7;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.workspace-sidebar__sections {
|
| 824 |
+
display: grid;
|
| 825 |
+
gap: 14px;
|
| 826 |
+
gap: 10px;
|
| 827 |
+
align-content: start;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.workspace-sidebar__section {
|
| 831 |
+
display: grid;
|
| 832 |
+
gap: 6px;
|
| 833 |
+
gap: 6px;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.workspace-sidebar__heading,
|
| 837 |
+
.patient-hero__eyebrow {
|
| 838 |
+
margin: 0;
|
| 839 |
+
font-size: 0.78rem;
|
| 840 |
+
font-weight: 700;
|
| 841 |
+
letter-spacing: 0.08em;
|
| 842 |
+
text-transform: uppercase;
|
| 843 |
+
color: #9ca3af;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.workspace-sidebar__nav,
|
| 847 |
+
.task-list,
|
| 848 |
+
.content-stack,
|
| 849 |
+
.metric-card__stack,
|
| 850 |
+
.workspace-header__breadcrumbs,
|
| 851 |
+
.workspace-header__actions,
|
| 852 |
+
.patient-hero__meta,
|
| 853 |
+
.workspace-search,
|
| 854 |
+
.note-list,
|
| 855 |
+
.order-list {
|
| 856 |
+
display: grid;
|
| 857 |
+
gap: 8px;
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
.workspace-sidebar__nav {
|
| 861 |
+
gap: 3px;
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
.workspace-sidebar__item {
|
| 865 |
+
display: flex;
|
| 866 |
+
align-items: center;
|
| 867 |
+
gap: 10px;
|
| 868 |
+
padding: 8px 10px;
|
| 869 |
+
border: 1px solid transparent;
|
| 870 |
+
border-radius: 10px;
|
| 871 |
+
color: #4b5563;
|
| 872 |
+
background: transparent;
|
| 873 |
+
font-weight: 600;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.workspace-sidebar__item:hover {
|
| 877 |
+
background: #f8fafc;
|
| 878 |
+
border-color: #e5e7eb;
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
.workspace-sidebar__item--active {
|
| 882 |
+
background: #ffffff;
|
| 883 |
+
border-color: #dbe3ee;
|
| 884 |
+
color: #1f3b63;
|
| 885 |
+
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
.workspace-sidebar__icon,
|
| 889 |
+
.workspace-icon-button,
|
| 890 |
+
.task-list__check {
|
| 891 |
+
width: 18px;
|
| 892 |
+
width: 16px;
|
| 893 |
+
height: 16px;
|
| 894 |
+
border-radius: 0;
|
| 895 |
+
display: inline-flex;
|
| 896 |
+
align-items: center;
|
| 897 |
+
justify-content: center;
|
| 898 |
+
background: transparent;
|
| 899 |
+
color: #55708f;
|
| 900 |
+
flex: 0 0 auto;
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
.workspace-sidebar__icon svg {
|
| 904 |
+
width: 16px;
|
| 905 |
+
height: 16px;
|
| 906 |
+
display: block;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.workspace-sidebar__footer,
|
| 910 |
+
.workspace-hero,
|
| 911 |
+
.patient-hero,
|
| 912 |
+
.metric-card,
|
| 913 |
+
.note-row,
|
| 914 |
+
.order-row,
|
| 915 |
+
.list-row,
|
| 916 |
+
.section-card,
|
| 917 |
+
.patient-card,
|
| 918 |
+
.patient-hero__panel,
|
| 919 |
+
.workspace-header {
|
| 920 |
+
border: 1px solid #e5e7eb;
|
| 921 |
+
background: #ffffff;
|
| 922 |
+
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.04);
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
.workspace-sidebar__footer {
|
| 926 |
+
padding: 16px;
|
| 927 |
+
border-radius: 20px;
|
| 928 |
+
display: grid;
|
| 929 |
+
gap: 10px;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
.workspace-sidebar__footer p,
|
| 933 |
+
.workspace-profile p,
|
| 934 |
+
.metric-card p,
|
| 935 |
+
.patient-hero p,
|
| 936 |
+
.workspace-hero p {
|
| 937 |
+
margin: 0;
|
| 938 |
+
color: #6b7280;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
.workspace-header {
|
| 942 |
+
display: flex;
|
| 943 |
+
justify-content: space-between;
|
| 944 |
+
align-items: center;
|
| 945 |
+
gap: 16px;
|
| 946 |
+
padding: 12px 16px;
|
| 947 |
+
border-radius: 24px;
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
.workspace-header--home {
|
| 951 |
+
justify-content: space-between;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.workspace-header--chart {
|
| 955 |
+
align-items: flex-start;
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
.workspace-search {
|
| 959 |
+
grid-template-columns: auto minmax(260px, 1fr);
|
| 960 |
+
align-items: center;
|
| 961 |
+
width: min(520px, 100%);
|
| 962 |
+
padding: 12px 14px;
|
| 963 |
+
border: 1px solid #e5e7eb;
|
| 964 |
+
border-radius: 18px;
|
| 965 |
+
background: #f9fafc;
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
.workspace-search input {
|
| 969 |
+
border: none;
|
| 970 |
+
padding: 0;
|
| 971 |
+
background: transparent;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
.workspace-search input:focus-visible {
|
| 975 |
+
outline: none;
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.workspace-search__icon {
|
| 979 |
+
color: #9ca3af;
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
.workspace-header__actions {
|
| 983 |
+
grid-auto-flow: column;
|
| 984 |
+
align-items: center;
|
| 985 |
+
justify-content: end;
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
.workspace-profile {
|
| 989 |
+
display: flex;
|
| 990 |
+
align-items: center;
|
| 991 |
+
gap: 10px;
|
| 992 |
+
padding-left: 4px;
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
.workspace-profile--panel {
|
| 996 |
+
padding: 8px 12px;
|
| 997 |
+
padding-left: 12px;
|
| 998 |
+
border: 1px solid #111111;
|
| 999 |
+
border-radius: 14px;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.workspace-profile--compact {
|
| 1003 |
+
padding-left: 0;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
.workspace-profile__avatar {
|
| 1007 |
+
width: 38px;
|
| 1008 |
+
height: 38px;
|
| 1009 |
+
border-radius: 14px;
|
| 1010 |
+
display: inline-flex;
|
| 1011 |
+
align-items: center;
|
| 1012 |
+
justify-content: center;
|
| 1013 |
+
background: #e8eef7;
|
| 1014 |
+
color: #35526f;
|
| 1015 |
+
font-weight: 700;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
.workspace-toggle,
|
| 1019 |
+
.workspace-backlink {
|
| 1020 |
+
display: inline-flex;
|
| 1021 |
+
align-items: center;
|
| 1022 |
+
gap: 8px;
|
| 1023 |
+
padding: 9px 12px;
|
| 1024 |
+
border: 1px solid #e5e7eb;
|
| 1025 |
+
border-radius: 999px;
|
| 1026 |
+
background: #f9fafc;
|
| 1027 |
+
color: #4b5563;
|
| 1028 |
+
font-weight: 600;
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
.workspace-pills {
|
| 1032 |
+
display: flex;
|
| 1033 |
+
gap: 8px;
|
| 1034 |
+
flex-wrap: wrap;
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.workspace-pills .nav-link,
|
| 1038 |
+
.workspace-pills--wide a {
|
| 1039 |
+
padding: 10px 14px;
|
| 1040 |
+
border: 1px solid #e5e7eb;
|
| 1041 |
+
border-radius: 999px;
|
| 1042 |
+
background: #f9fafc;
|
| 1043 |
+
font-size: 0.9rem;
|
| 1044 |
+
font-weight: 600;
|
| 1045 |
+
color: #4b5563;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
.workspace-pills .nav-link.is-active,
|
| 1049 |
+
.workspace-pills--wide a.is-active {
|
| 1050 |
+
background: #eef4ff;
|
| 1051 |
+
border-color: #cddcf4;
|
| 1052 |
+
color: #27476f;
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
.workspace-hero,
|
| 1056 |
+
.patient-hero {
|
| 1057 |
+
border-radius: 28px;
|
| 1058 |
+
padding: 24px;
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
.workspace-hero {
|
| 1062 |
+
display: flex;
|
| 1063 |
+
justify-content: space-between;
|
| 1064 |
+
align-items: center;
|
| 1065 |
+
gap: 18px;
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
.workspace-hero h1,
|
| 1069 |
+
.patient-hero h1 {
|
| 1070 |
+
margin: 0 0 6px;
|
| 1071 |
+
font-size: 2rem;
|
| 1072 |
+
letter-spacing: -0.04em;
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
.metric-grid {
|
| 1076 |
+
display: grid;
|
| 1077 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 1078 |
+
gap: 16px;
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
.metric-grid--chart {
|
| 1082 |
+
align-items: stretch;
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
.metric-card {
|
| 1086 |
+
border-radius: 22px;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
.metric-card--wide {
|
| 1090 |
+
grid-column: span 2;
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
.metric-card__value {
|
| 1094 |
+
font-size: 2rem;
|
| 1095 |
+
font-weight: 700;
|
| 1096 |
+
letter-spacing: -0.04em;
|
| 1097 |
+
color: #172554;
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
.content-grid {
|
| 1101 |
+
display: grid;
|
| 1102 |
+
gap: 16px;
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
.content-grid--home {
|
| 1106 |
+
grid-template-columns: minmax(0, 1.65fr) minmax(320px, 0.85fr);
|
| 1107 |
+
}
|
| 1108 |
+
|
| 1109 |
+
.content-grid--chart {
|
| 1110 |
+
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.85fr);
|
| 1111 |
+
}
|
| 1112 |
+
|
| 1113 |
+
.content-stack {
|
| 1114 |
+
align-content: start;
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
.task-list__item {
|
| 1118 |
+
display: grid;
|
| 1119 |
+
grid-template-columns: auto 1fr;
|
| 1120 |
+
gap: 12px;
|
| 1121 |
+
align-items: start;
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
.task-list__check::before {
|
| 1125 |
+
content: "✓";
|
| 1126 |
+
font-weight: 700;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
.patient-hero {
|
| 1130 |
+
display: grid;
|
| 1131 |
+
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.9fr);
|
| 1132 |
+
gap: 18px;
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
.patient-hero__identity {
|
| 1136 |
+
display: flex;
|
| 1137 |
+
gap: 16px;
|
| 1138 |
+
align-items: center;
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
+
.patient-hero__panel {
|
| 1142 |
+
padding: 16px;
|
| 1143 |
+
border-radius: 20px;
|
| 1144 |
+
display: grid;
|
| 1145 |
+
gap: 6px;
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
.patient-hero__panel--status {
|
| 1149 |
+
align-content: start;
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
.patient-hero__panel strong {
|
| 1153 |
+
font-size: 0.78rem;
|
| 1154 |
+
text-transform: uppercase;
|
| 1155 |
+
letter-spacing: 0.08em;
|
| 1156 |
+
color: #9ca3af;
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
.list-row--split {
|
| 1160 |
+
display: flex;
|
| 1161 |
+
justify-content: space-between;
|
| 1162 |
+
align-items: center;
|
| 1163 |
+
gap: 16px;
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
.list-row--split p {
|
| 1167 |
+
margin: 0;
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
.section-card,
|
| 1171 |
+
.patient-card,
|
| 1172 |
+
.note-row,
|
| 1173 |
+
.order-row,
|
| 1174 |
+
.list-row {
|
| 1175 |
+
border-radius: 22px;
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
.section-card__header {
|
| 1179 |
+
padding-bottom: 14px;
|
| 1180 |
+
border-bottom: 1px solid #edf2f7;
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
.section-card__header h2 {
|
| 1184 |
+
font-size: 1.02rem;
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
.patient-card:hover,
|
| 1188 |
+
.workspace-backlink:hover,
|
| 1189 |
+
.workspace-icon-button:hover,
|
| 1190 |
+
.workspace-toggle:hover,
|
| 1191 |
+
.workspace-pills .nav-link:hover,
|
| 1192 |
+
.workspace-pills--wide a:hover {
|
| 1193 |
+
border-color: #d4dde8;
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
@media (max-width: 1240px) {
|
| 1197 |
+
.dashboard-shell,
|
| 1198 |
+
.patient-hero,
|
| 1199 |
+
.content-grid--home,
|
| 1200 |
+
.content-grid--chart,
|
| 1201 |
+
.metric-grid {
|
| 1202 |
+
grid-template-columns: 1fr;
|
| 1203 |
+
}
|
| 1204 |
+
|
| 1205 |
+
.workspace-sidebar {
|
| 1206 |
+
position: static;
|
| 1207 |
+
min-height: auto;
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
.metric-card--wide {
|
| 1211 |
+
grid-column: auto;
|
| 1212 |
+
}
|
| 1213 |
+
}
|
| 1214 |
+
|
| 1215 |
+
@media (max-width: 900px) {
|
| 1216 |
+
.dashboard-shell {
|
| 1217 |
+
padding: 14px;
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
.workspace-header,
|
| 1221 |
+
.workspace-hero,
|
| 1222 |
+
.patient-hero,
|
| 1223 |
+
.list-row--split {
|
| 1224 |
+
flex-direction: column;
|
| 1225 |
+
align-items: flex-start;
|
| 1226 |
+
}
|
| 1227 |
+
|
| 1228 |
+
.workspace-header__actions {
|
| 1229 |
+
grid-auto-flow: row;
|
| 1230 |
+
justify-items: start;
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
.workspace-search {
|
| 1234 |
+
width: 100%;
|
| 1235 |
+
grid-template-columns: auto 1fr;
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
+
.ehr-table__header,
|
| 1239 |
+
.patient-card--table {
|
| 1240 |
+
grid-template-columns: 1fr;
|
| 1241 |
+
}
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
.dashboard-shell {
|
| 1245 |
+
position: relative;
|
| 1246 |
+
isolation: isolate;
|
| 1247 |
+
background: #f3f4f6 !important;
|
| 1248 |
+
}
|
| 1249 |
+
|
| 1250 |
+
.dashboard-shell::before {
|
| 1251 |
+
content: none;
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
.dashboard-shell::after {
|
| 1255 |
+
content: "";
|
| 1256 |
+
position: fixed;
|
| 1257 |
+
inset: 0;
|
| 1258 |
+
background-image: linear-gradient(rgba(75, 85, 99, 0.18) 1px, transparent 1px), linear-gradient(90deg, rgba(75, 85, 99, 0.18) 1px, transparent 1px);
|
| 1259 |
+
background-size: 28px 28px;
|
| 1260 |
+
opacity: 0.04;
|
| 1261 |
+
z-index: -3;
|
| 1262 |
+
pointer-events: none;
|
| 1263 |
+
}
|
| 1264 |
+
|
| 1265 |
+
body,
|
| 1266 |
+
.dashboard-main,
|
| 1267 |
+
.workspace-sidebar,
|
| 1268 |
+
.workspace-header,
|
| 1269 |
+
.workspace-hero,
|
| 1270 |
+
.patient-hero,
|
| 1271 |
+
.metric-card,
|
| 1272 |
+
.section-card,
|
| 1273 |
+
.note-row,
|
| 1274 |
+
.order-row,
|
| 1275 |
+
.list-row,
|
| 1276 |
+
.patient-card,
|
| 1277 |
+
.patient-hero__panel,
|
| 1278 |
+
.workspace-sidebar__footer,
|
| 1279 |
+
.patient-summary-card,
|
| 1280 |
+
.patient-sidebar-card,
|
| 1281 |
+
.patient-header,
|
| 1282 |
+
.epic-contextbar,
|
| 1283 |
+
.chart-activity-bar,
|
| 1284 |
+
.workspace-search,
|
| 1285 |
+
.problem-list__item,
|
| 1286 |
+
.table th,
|
| 1287 |
+
.table td,
|
| 1288 |
+
.subtab-strip__item,
|
| 1289 |
+
.workspace-sidebar__item,
|
| 1290 |
+
.workspace-toggle,
|
| 1291 |
+
.workspace-backlink,
|
| 1292 |
+
.workspace-icon-button,
|
| 1293 |
+
.summary-flag,
|
| 1294 |
+
.badge,
|
| 1295 |
+
.status-pill,
|
| 1296 |
+
.epic-tab,
|
| 1297 |
+
.epic-topbar__tabs .nav-link,
|
| 1298 |
+
.workspace-pills .nav-link,
|
| 1299 |
+
.workspace-pills--wide a,
|
| 1300 |
+
.sidebar-nav a,
|
| 1301 |
+
.secondary-button {
|
| 1302 |
+
color: #111111 !important;
|
| 1303 |
+
border-color: #111111 !important;
|
| 1304 |
+
box-shadow: none !important;
|
| 1305 |
+
}
|
| 1306 |
+
|
| 1307 |
+
body,
|
| 1308 |
+
.dashboard-main {
|
| 1309 |
+
background: transparent !important;
|
| 1310 |
+
}
|
| 1311 |
+
|
| 1312 |
+
.workspace-sidebar,
|
| 1313 |
+
.workspace-header,
|
| 1314 |
+
.workspace-hero,
|
| 1315 |
+
.patient-hero,
|
| 1316 |
+
.metric-card,
|
| 1317 |
+
.section-card,
|
| 1318 |
+
.note-row,
|
| 1319 |
+
.order-row,
|
| 1320 |
+
.list-row,
|
| 1321 |
+
.patient-card,
|
| 1322 |
+
.patient-hero__panel,
|
| 1323 |
+
.workspace-sidebar__footer,
|
| 1324 |
+
.patient-summary-card,
|
| 1325 |
+
.patient-sidebar-card,
|
| 1326 |
+
.patient-header,
|
| 1327 |
+
.epic-contextbar,
|
| 1328 |
+
.chart-activity-bar,
|
| 1329 |
+
.workspace-search,
|
| 1330 |
+
.problem-list__item,
|
| 1331 |
+
.table th,
|
| 1332 |
+
.table td,
|
| 1333 |
+
.subtab-strip__item,
|
| 1334 |
+
.workspace-sidebar__item,
|
| 1335 |
+
.workspace-toggle,
|
| 1336 |
+
.workspace-backlink,
|
| 1337 |
+
.workspace-icon-button,
|
| 1338 |
+
.summary-flag,
|
| 1339 |
+
.badge,
|
| 1340 |
+
.status-pill,
|
| 1341 |
+
.epic-tab,
|
| 1342 |
+
.epic-topbar__tabs .nav-link,
|
| 1343 |
+
.workspace-pills .nav-link,
|
| 1344 |
+
.workspace-pills--wide a,
|
| 1345 |
+
.sidebar-nav a,
|
| 1346 |
+
.secondary-button,
|
| 1347 |
+
input,
|
| 1348 |
+
textarea,
|
| 1349 |
+
select {
|
| 1350 |
+
color: #111111 !important;
|
| 1351 |
+
border-color: #111111 !important;
|
| 1352 |
+
background: #ffffff !important;
|
| 1353 |
+
}
|
| 1354 |
+
|
| 1355 |
+
.workspace-header,
|
| 1356 |
+
.workspace-sidebar,
|
| 1357 |
+
.workspace-hero,
|
| 1358 |
+
.patient-hero {
|
| 1359 |
+
border-radius: 30px !important;
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
.workspace-sidebar__icon,
|
| 1363 |
+
.task-list__check,
|
| 1364 |
+
.workspace-profile__avatar,
|
| 1365 |
+
.patient-avatar,
|
| 1366 |
+
.patient-summary-card__avatar {
|
| 1367 |
+
background: transparent !important;
|
| 1368 |
+
color: #111111 !important;
|
| 1369 |
+
border: none !important;
|
| 1370 |
+
}
|
| 1371 |
+
|
| 1372 |
+
.primary-button,
|
| 1373 |
+
.subtab-strip__item--active,
|
| 1374 |
+
.chart-activity-bar a.is-active,
|
| 1375 |
+
.sidebar-nav a.is-active,
|
| 1376 |
+
.workspace-sidebar__item--active,
|
| 1377 |
+
.workspace-pills .nav-link.is-active,
|
| 1378 |
+
.workspace-pills--wide a.is-active,
|
| 1379 |
+
.epic-tab--active,
|
| 1380 |
+
.epic-topbar__tabs .nav-link.is-active,
|
| 1381 |
+
.summary-flag--accent,
|
| 1382 |
+
.problem-list__status {
|
| 1383 |
+
background: #111111 !important;
|
| 1384 |
+
color: #ffffff !important;
|
| 1385 |
+
border-color: #111111 !important;
|
| 1386 |
+
}
|
| 1387 |
+
|
| 1388 |
+
.status-pill[data-status="OPEN"],
|
| 1389 |
+
.status-pill[data-status="DRAFT"],
|
| 1390 |
+
.status-pill[data-status="PENDING_SIGNATURE"],
|
| 1391 |
+
.status-pill[data-status="SIGNED"],
|
| 1392 |
+
.status-pill[data-status="CLOSED"],
|
| 1393 |
+
.problem-list__status--muted {
|
| 1394 |
+
background: transparent !important;
|
| 1395 |
+
color: #111111 !important;
|
| 1396 |
+
border: 1px solid #111111 !important;
|
| 1397 |
+
}
|
| 1398 |
+
|
| 1399 |
+
.muted,
|
| 1400 |
+
.section-card p,
|
| 1401 |
+
.workspace-profile p,
|
| 1402 |
+
.metric-card p,
|
| 1403 |
+
.patient-hero p,
|
| 1404 |
+
.workspace-hero p,
|
| 1405 |
+
.summary-panel__row span,
|
| 1406 |
+
.rail-list__item span,
|
| 1407 |
+
.problem-list__item span:last-child,
|
| 1408 |
+
.table th,
|
| 1409 |
+
.ehr-table__header,
|
| 1410 |
+
.patient-fact strong,
|
| 1411 |
+
.workspace-sidebar__heading,
|
| 1412 |
+
.patient-hero__eyebrow,
|
| 1413 |
+
.patient-sidebar-card__label {
|
| 1414 |
+
color: #525252 !important;
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
.app-brand__copy strong,
|
| 1418 |
+
.app-brand__copy span,
|
| 1419 |
+
.workspace-hero h1,
|
| 1420 |
+
.patient-hero h1,
|
| 1421 |
+
.patient-hero__title,
|
| 1422 |
+
.metric-card__value,
|
| 1423 |
+
.section-card__header h2,
|
| 1424 |
+
.note-row h3,
|
| 1425 |
+
.order-row h3,
|
| 1426 |
+
.patient-card strong,
|
| 1427 |
+
.workspace-hero__location {
|
| 1428 |
+
color: #111111 !important;
|
| 1429 |
+
}
|
| 1430 |
+
|
| 1431 |
+
.workspace-hero {
|
| 1432 |
+
align-items: flex-start !important;
|
| 1433 |
+
min-height: 240px;
|
| 1434 |
+
min-height: 200px;
|
| 1435 |
+
}
|
| 1436 |
+
|
| 1437 |
+
.workspace-hero__copy,
|
| 1438 |
+
.workspace-hero__aside {
|
| 1439 |
+
display: grid;
|
| 1440 |
+
gap: 14px;
|
| 1441 |
+
}
|
| 1442 |
+
|
| 1443 |
+
.workspace-hero h1 {
|
| 1444 |
+
display: block;
|
| 1445 |
+
font-size: clamp(1.6rem, 3vw, 2.2rem) !important;
|
| 1446 |
+
line-height: 1.05 !important;
|
| 1447 |
+
letter-spacing: -0.03em;
|
| 1448 |
+
text-transform: none;
|
| 1449 |
+
}
|
| 1450 |
+
|
| 1451 |
+
.workspace-hero__eyebrow,
|
| 1452 |
+
.workspace-hero__location,
|
| 1453 |
+
.workspace-sidebar__heading,
|
| 1454 |
+
.workspace-toggle,
|
| 1455 |
+
.workspace-backlink,
|
| 1456 |
+
.badge,
|
| 1457 |
+
.status-pill,
|
| 1458 |
+
.summary-flag,
|
| 1459 |
+
.workspace-pills .nav-link,
|
| 1460 |
+
.workspace-pills--wide a,
|
| 1461 |
+
.workspace-sidebar__item,
|
| 1462 |
+
.sidebar-nav a,
|
| 1463 |
+
.epic-tab,
|
| 1464 |
+
.epic-topbar__tabs .nav-link,
|
| 1465 |
+
.subtab-strip__item,
|
| 1466 |
+
.section-card__header p,
|
| 1467 |
+
.app-brand__copy span {
|
| 1468 |
+
text-transform: uppercase;
|
| 1469 |
+
letter-spacing: 0.08em;
|
| 1470 |
+
font-size: 0.76rem !important;
|
| 1471 |
+
}
|
| 1472 |
+
|
| 1473 |
+
.workspace-hero__lede,
|
| 1474 |
+
.patient-hero__lede,
|
| 1475 |
+
.note-row p,
|
| 1476 |
+
.order-row p,
|
| 1477 |
+
.list-row p,
|
| 1478 |
+
.summary-panel,
|
| 1479 |
+
.task-list,
|
| 1480 |
+
.table td,
|
| 1481 |
+
textarea,
|
| 1482 |
+
input,
|
| 1483 |
+
select {
|
| 1484 |
+
font-family: var(--font-ui), "IBM Plex Sans", "Segoe UI", system-ui, sans-serif !important;
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
.workspace-hero__location {
|
| 1488 |
+
justify-self: start;
|
| 1489 |
+
padding: 8px 10px;
|
| 1490 |
+
border: 1px solid #111111;
|
| 1491 |
+
border-radius: 999px;
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
.workspace-hero__aside {
|
| 1495 |
+
justify-items: start;
|
| 1496 |
+
align-self: end;
|
| 1497 |
+
align-self: start;
|
| 1498 |
+
}
|
| 1499 |
+
|
| 1500 |
+
.patient-hero {
|
| 1501 |
+
grid-template-columns: minmax(0, 1.25fr) minmax(320px, 0.9fr) !important;
|
| 1502 |
+
}
|
| 1503 |
+
|
| 1504 |
+
.patient-hero__identity {
|
| 1505 |
+
align-items: start !important;
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
.patient-hero__title {
|
| 1509 |
+
font-size: clamp(1.85rem, 3.4vw, 2.8rem) !important;
|
| 1510 |
+
line-height: 1.02 !important;
|
| 1511 |
+
letter-spacing: -0.03em;
|
| 1512 |
+
text-transform: none;
|
| 1513 |
+
margin: 0;
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
.workspace-search__icon,
|
| 1517 |
+
.workspace-icon-button,
|
| 1518 |
+
.workspace-sidebar__icon {
|
| 1519 |
+
color: #111111 !important;
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
.workspace-icon-button,
|
| 1523 |
+
.workspace-sidebar__icon {
|
| 1524 |
+
background: transparent !important;
|
| 1525 |
+
}
|
| 1526 |
+
|
| 1527 |
+
.patient-card:hover,
|
| 1528 |
+
.workspace-backlink:hover,
|
| 1529 |
+
.workspace-icon-button:hover,
|
| 1530 |
+
.workspace-toggle:hover,
|
| 1531 |
+
.workspace-pills .nav-link:hover,
|
| 1532 |
+
.workspace-pills--wide a:hover,
|
| 1533 |
+
.workspace-sidebar__item:hover,
|
| 1534 |
+
.sidebar-nav a:hover,
|
| 1535 |
+
.primary-button:hover,
|
| 1536 |
+
.secondary-button:hover {
|
| 1537 |
+
background: #111111 !important;
|
| 1538 |
+
color: #ffffff !important;
|
| 1539 |
+
border-color: #111111 !important;
|
| 1540 |
+
}
|
| 1541 |
+
|
| 1542 |
+
.workspace-icon-button:hover,
|
| 1543 |
+
.workspace-toggle:hover,
|
| 1544 |
+
.workspace-backlink:hover,
|
| 1545 |
+
.workspace-pills .nav-link:hover,
|
| 1546 |
+
.workspace-pills--wide a:hover,
|
| 1547 |
+
.workspace-sidebar__item:hover,
|
| 1548 |
+
.sidebar-nav a:hover,
|
| 1549 |
+
.secondary-button:hover,
|
| 1550 |
+
.primary-button:hover {
|
| 1551 |
+
text-decoration: none;
|
| 1552 |
+
}
|
| 1553 |
+
|
| 1554 |
+
.workspace-sidebar__item:hover .workspace-sidebar__icon,
|
| 1555 |
+
.workspace-sidebar__item--active .workspace-sidebar__icon,
|
| 1556 |
+
.workspace-icon-button:hover,
|
| 1557 |
+
.workspace-toggle:hover,
|
| 1558 |
+
.workspace-backlink:hover,
|
| 1559 |
+
.workspace-pills .nav-link:hover,
|
| 1560 |
+
.workspace-pills--wide a:hover,
|
| 1561 |
+
.sidebar-nav a:hover {
|
| 1562 |
+
color: #ffffff !important;
|
| 1563 |
+
}
|
| 1564 |
+
|
| 1565 |
+
.workspace-sidebar__item:hover .workspace-sidebar__icon {
|
| 1566 |
+
background: transparent !important;
|
| 1567 |
+
border-color: transparent !important;
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
.patient-card:hover {
|
| 1571 |
+
background: #111111 !important;
|
| 1572 |
+
color: #ffffff !important;
|
| 1573 |
+
border-color: #111111 !important;
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
.workspace-toggle,
|
| 1577 |
+
.workspace-backlink,
|
| 1578 |
+
.workspace-icon-button,
|
| 1579 |
+
.primary-button,
|
| 1580 |
+
.secondary-button {
|
| 1581 |
+
padding: 8px 11px !important;
|
| 1582 |
+
min-height: 0 !important;
|
| 1583 |
+
border-radius: 10px !important;
|
| 1584 |
+
}
|
| 1585 |
+
|
| 1586 |
+
.workspace-toggle--static {
|
| 1587 |
+
display: inline-flex;
|
| 1588 |
+
align-items: center;
|
| 1589 |
+
gap: 8px;
|
| 1590 |
+
cursor: default;
|
| 1591 |
+
}
|
| 1592 |
+
|
| 1593 |
+
.workspace-toggle--static:hover {
|
| 1594 |
+
background: #ffffff !important;
|
| 1595 |
+
color: #111111 !important;
|
| 1596 |
+
border-color: #111111 !important;
|
| 1597 |
+
}
|
| 1598 |
+
|
| 1599 |
+
.workspace-toggle__icon {
|
| 1600 |
+
width: 14px;
|
| 1601 |
+
height: 14px;
|
| 1602 |
+
display: inline-flex;
|
| 1603 |
+
align-items: center;
|
| 1604 |
+
justify-content: center;
|
| 1605 |
+
flex: 0 0 auto;
|
| 1606 |
+
padding: 3px;
|
| 1607 |
+
border: 1px solid currentColor;
|
| 1608 |
+
border-radius: 999px;
|
| 1609 |
+
box-sizing: content-box;
|
| 1610 |
+
}
|
| 1611 |
+
|
| 1612 |
+
.workspace-toggle__icon svg {
|
| 1613 |
+
width: 14px;
|
| 1614 |
+
height: 14px;
|
| 1615 |
+
display: block;
|
| 1616 |
+
}
|
| 1617 |
+
|
| 1618 |
+
.workspace-profile--panel .workspace-profile__avatar {
|
| 1619 |
+
border: 1px solid #111111 !important;
|
| 1620 |
+
border-radius: 999px;
|
| 1621 |
+
}
|
| 1622 |
+
|
| 1623 |
+
.workspace-sidebar__icon {
|
| 1624 |
+
font-size: 0.78rem !important;
|
| 1625 |
+
font-weight: 700;
|
| 1626 |
+
line-height: 1;
|
| 1627 |
+
}
|
| 1628 |
+
|
| 1629 |
+
.patient-card:hover .muted,
|
| 1630 |
+
.patient-card:hover .status-pill,
|
| 1631 |
+
.patient-card:hover .summary-flag,
|
| 1632 |
+
.patient-card:hover strong,
|
| 1633 |
+
.patient-card:hover p {
|
| 1634 |
+
color: #ffffff !important;
|
| 1635 |
+
border-color: #ffffff !important;
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
.patient-card:hover .status-pill {
|
| 1639 |
+
background: transparent !important;
|
| 1640 |
+
}
|
| 1641 |
+
|
| 1642 |
+
@media (max-width: 900px) {
|
| 1643 |
+
.dashboard-shell::after {
|
| 1644 |
+
background-size: 20px 20px;
|
| 1645 |
+
}
|
| 1646 |
+
}
|
apps/ehr/app/layout.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode } from "react";
|
| 2 |
+
|
| 3 |
+
import type { Metadata } from "next";
|
| 4 |
+
import { IBM_Plex_Sans, Source_Serif_4 } from "next/font/google";
|
| 5 |
+
import ehrgymIcon from "../../../ehrgym_icon.png";
|
| 6 |
+
|
| 7 |
+
import "./globals.css";
|
| 8 |
+
|
| 9 |
+
const uiFont = IBM_Plex_Sans({
|
| 10 |
+
subsets: ["latin"],
|
| 11 |
+
weight: ["400", "500", "600", "700"],
|
| 12 |
+
variable: "--font-ui"
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
const bodyFont = Source_Serif_4({
|
| 16 |
+
subsets: ["latin"],
|
| 17 |
+
weight: ["400", "500", "600", "700"],
|
| 18 |
+
variable: "--font-body"
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
export const metadata: Metadata = {
|
| 22 |
+
title: "EHRGym",
|
| 23 |
+
description: "Clinical charting workspace for computer-use agents",
|
| 24 |
+
icons: {
|
| 25 |
+
icon: ehrgymIcon.src,
|
| 26 |
+
shortcut: ehrgymIcon.src,
|
| 27 |
+
apple: ehrgymIcon.src
|
| 28 |
+
}
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
| 32 |
+
return (
|
| 33 |
+
<html lang="en">
|
| 34 |
+
<body className={`${uiFont.variable} ${bodyFont.variable}`}>{children}</body>
|
| 35 |
+
</html>
|
| 36 |
+
);
|
| 37 |
+
}
|
apps/ehr/app/page.tsx
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link";
|
| 2 |
+
|
| 3 |
+
import { AppBrand } from "../components/app-brand";
|
| 4 |
+
import { SectionCard } from "../components/section-card";
|
| 5 |
+
import { WorkspaceSidebar } from "../components/workspace-sidebar";
|
| 6 |
+
import { formatDateTime, parseJsonValue } from "../lib/chart";
|
| 7 |
+
import { prisma } from "../lib/db";
|
| 8 |
+
|
| 9 |
+
type HomePatient = {
|
| 10 |
+
id: string;
|
| 11 |
+
mrn: string;
|
| 12 |
+
fullName: string;
|
| 13 |
+
age: number;
|
| 14 |
+
sex: string;
|
| 15 |
+
summary: string;
|
| 16 |
+
bannerFlagsJson: string;
|
| 17 |
+
encounters: Array<{
|
| 18 |
+
status: string;
|
| 19 |
+
reasonForVisit: string;
|
| 20 |
+
startedAt: Date;
|
| 21 |
+
}>;
|
| 22 |
+
scenarios: Array<{
|
| 23 |
+
objective: string;
|
| 24 |
+
}>;
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export default async function HomePage() {
|
| 28 |
+
const patients: HomePatient[] = await prisma.patient.findMany({
|
| 29 |
+
orderBy: { fullName: "asc" },
|
| 30 |
+
include: {
|
| 31 |
+
encounters: {
|
| 32 |
+
take: 1,
|
| 33 |
+
orderBy: { startedAt: "desc" }
|
| 34 |
+
},
|
| 35 |
+
scenarios: {
|
| 36 |
+
take: 1,
|
| 37 |
+
orderBy: { createdAt: "desc" }
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
const leadPatient = patients[0];
|
| 43 |
+
const leadEncounter = leadPatient?.encounters[0];
|
| 44 |
+
const leadFlags = leadPatient ? parseJsonValue<string[]>(leadPatient.bannerFlagsJson) : [];
|
| 45 |
+
const openCount = patients.filter((patient) => patient.encounters[0]?.status === "OPEN").length;
|
| 46 |
+
|
| 47 |
+
return (
|
| 48 |
+
<main className="dashboard-shell">
|
| 49 |
+
<WorkspaceSidebar
|
| 50 |
+
brand={<AppBrand title="EHRGym" subtitle="Clinical workspace" href="/" />}
|
| 51 |
+
sections={[
|
| 52 |
+
{
|
| 53 |
+
title: "Navigation",
|
| 54 |
+
items: [
|
| 55 |
+
{ label: "Dashboard", icon: "dashboard", href: "/" },
|
| 56 |
+
{ label: "Patient list", icon: "patients", href: "#patient-list-section" },
|
| 57 |
+
{ label: "Recent Activity", icon: "activity", href: "#recent-activity" },
|
| 58 |
+
{ label: "Snapshot", icon: "snapshot", href: "#selected-chart" }
|
| 59 |
+
]
|
| 60 |
+
}
|
| 61 |
+
]}
|
| 62 |
+
footerTitle="Operational View"
|
| 63 |
+
footerText="Prepared for rounding, documentation, and order entry from a single dashboard."
|
| 64 |
+
/>
|
| 65 |
+
|
| 66 |
+
<div className="dashboard-main">
|
| 67 |
+
<header className="workspace-header workspace-header--home" data-testid="patient-list-hero">
|
| 68 |
+
<span className="workspace-toggle workspace-toggle--static">
|
| 69 |
+
<span className="workspace-toggle__icon" aria-hidden="true">
|
| 70 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
|
| 71 |
+
<path d="M10 17s4.5-4.7 4.5-8A4.5 4.5 0 1 0 5.5 9c0 3.3 4.5 8 4.5 8Z" />
|
| 72 |
+
<circle cx="10" cy="9" r="1.6" />
|
| 73 |
+
</svg>
|
| 74 |
+
</span>
|
| 75 |
+
<span>5 West Med-Surg</span>
|
| 76 |
+
</span>
|
| 77 |
+
<div className="workspace-profile workspace-profile--panel">
|
| 78 |
+
<div className="workspace-profile__avatar">PS</div>
|
| 79 |
+
<div>
|
| 80 |
+
<strong>Patrick Sullivan</strong>
|
| 81 |
+
<p>RN</p>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
</header>
|
| 85 |
+
|
| 86 |
+
<section className="metric-grid">
|
| 87 |
+
<SectionCard title="Open Charts" subtitle="Ready for review" className="metric-card">
|
| 88 |
+
<div className="metric-card__value">{openCount}</div>
|
| 89 |
+
<p>{patients.length} total patients on service</p>
|
| 90 |
+
</SectionCard>
|
| 91 |
+
<SectionCard title="Orders Pending" subtitle="Awaiting attention" className="metric-card">
|
| 92 |
+
<div className="metric-card__value">{leadEncounter?.status === "OPEN" ? "1/3" : "0/3"}</div>
|
| 93 |
+
<p>Prioritize chart completion before sign-off.</p>
|
| 94 |
+
</SectionCard>
|
| 95 |
+
<SectionCard title="Next Review" subtitle="Current focus" className="metric-card metric-card--wide">
|
| 96 |
+
<div className="metric-card__stack">
|
| 97 |
+
<strong>{leadPatient?.fullName ?? "No patient selected"}</strong>
|
| 98 |
+
<p>{leadEncounter?.reasonForVisit ?? "No active encounter"}</p>
|
| 99 |
+
<span className="summary-flag summary-flag--accent">{leadEncounter ? formatDateTime(leadEncounter.startedAt) : "Pending"}</span>
|
| 100 |
+
</div>
|
| 101 |
+
</SectionCard>
|
| 102 |
+
</section>
|
| 103 |
+
|
| 104 |
+
<div className="content-grid content-grid--home">
|
| 105 |
+
<div className="content-stack">
|
| 106 |
+
<SectionCard title="Patient List" subtitle="Select a patient to enter the chart" testId="patient-list">
|
| 107 |
+
<div id="patient-list-section" />
|
| 108 |
+
<div className="ehr-table__header">
|
| 109 |
+
<span>Name</span>
|
| 110 |
+
<span>MRN</span>
|
| 111 |
+
<span>Visit</span>
|
| 112 |
+
<span>Status</span>
|
| 113 |
+
<span>Last update</span>
|
| 114 |
+
</div>
|
| 115 |
+
<div className="patient-list patient-list--table">
|
| 116 |
+
{patients.map((patient: HomePatient) => {
|
| 117 |
+
const encounter = patient.encounters[0];
|
| 118 |
+
const scenario = patient.scenarios[0];
|
| 119 |
+
const flags = parseJsonValue<string[]>(patient.bannerFlagsJson);
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<Link
|
| 123 |
+
key={patient.id}
|
| 124 |
+
className="patient-card patient-card--table"
|
| 125 |
+
href={`/patient/${patient.id}`}
|
| 126 |
+
data-testid={`patient-card-${patient.id}`}
|
| 127 |
+
>
|
| 128 |
+
<div>
|
| 129 |
+
<strong>{patient.fullName}</strong>
|
| 130 |
+
<p className="muted">
|
| 131 |
+
{patient.age} y/o {patient.sex}
|
| 132 |
+
</p>
|
| 133 |
+
</div>
|
| 134 |
+
<div>
|
| 135 |
+
<strong>{patient.mrn}</strong>
|
| 136 |
+
<p className="muted">General medicine</p>
|
| 137 |
+
</div>
|
| 138 |
+
<div>
|
| 139 |
+
<strong>{encounter?.reasonForVisit ?? "No encounter"}</strong>
|
| 140 |
+
<p className="muted">{patient.summary}</p>
|
| 141 |
+
</div>
|
| 142 |
+
<div>
|
| 143 |
+
<span className="status-pill" data-status={encounter?.status ?? "OPEN"}>
|
| 144 |
+
{encounter?.status ?? "OPEN"}
|
| 145 |
+
</span>
|
| 146 |
+
</div>
|
| 147 |
+
<div>
|
| 148 |
+
<strong>{encounter ? formatDateTime(encounter.startedAt) : "Pending"}</strong>
|
| 149 |
+
<p className="muted">{scenario ? scenario.objective : flags.join(" · ")}</p>
|
| 150 |
+
</div>
|
| 151 |
+
</Link>
|
| 152 |
+
);
|
| 153 |
+
})}
|
| 154 |
+
</div>
|
| 155 |
+
</SectionCard>
|
| 156 |
+
|
| 157 |
+
<SectionCard title="Recent Chart Activity" subtitle="Review recent notes and care context">
|
| 158 |
+
<div id="recent-activity" />
|
| 159 |
+
<div className="note-list">
|
| 160 |
+
{patients.slice(0, 2).map((patient) => {
|
| 161 |
+
const encounter = patient.encounters[0];
|
| 162 |
+
const scenario = patient.scenarios[0];
|
| 163 |
+
|
| 164 |
+
return (
|
| 165 |
+
<article key={patient.id} className="note-row">
|
| 166 |
+
<header>
|
| 167 |
+
<div>
|
| 168 |
+
<h3>{patient.fullName}</h3>
|
| 169 |
+
<p className="muted">{encounter?.reasonForVisit ?? "No encounter"}</p>
|
| 170 |
+
</div>
|
| 171 |
+
<span className="muted">{encounter ? formatDateTime(encounter.startedAt) : "Pending"}</span>
|
| 172 |
+
</header>
|
| 173 |
+
<p>{patient.summary}</p>
|
| 174 |
+
{scenario ? <span className="summary-flag">{scenario.objective}</span> : null}
|
| 175 |
+
</article>
|
| 176 |
+
);
|
| 177 |
+
})}
|
| 178 |
+
</div>
|
| 179 |
+
</SectionCard>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<aside className="content-stack">
|
| 183 |
+
<SectionCard title="Selected Chart Snapshot" subtitle={leadPatient?.fullName ?? "No patient selected"} className="section-card--summary">
|
| 184 |
+
<div id="selected-chart" />
|
| 185 |
+
{leadPatient && leadEncounter ? (
|
| 186 |
+
<div className="summary-panel">
|
| 187 |
+
<div className="summary-panel__row">
|
| 188 |
+
<strong>MRN</strong>
|
| 189 |
+
<span>{leadPatient.mrn}</span>
|
| 190 |
+
</div>
|
| 191 |
+
<div className="summary-panel__row">
|
| 192 |
+
<strong>Visit</strong>
|
| 193 |
+
<span>{leadEncounter.reasonForVisit}</span>
|
| 194 |
+
</div>
|
| 195 |
+
<div className="summary-panel__row">
|
| 196 |
+
<strong>Provider</strong>
|
| 197 |
+
<span>Open chart to continue workflow</span>
|
| 198 |
+
</div>
|
| 199 |
+
<div className="summary-flags">
|
| 200 |
+
{leadFlags.map((flag) => (
|
| 201 |
+
<span key={flag} className="summary-flag">
|
| 202 |
+
{flag}
|
| 203 |
+
</span>
|
| 204 |
+
))}
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
) : null}
|
| 208 |
+
</SectionCard>
|
| 209 |
+
|
| 210 |
+
<SectionCard title="Today’s Tasks" subtitle="Priority items for chart completion" className="section-card--summary">
|
| 211 |
+
<div id="today-tasks" />
|
| 212 |
+
<div className="task-list">
|
| 213 |
+
<div className="task-list__item">
|
| 214 |
+
<span className="task-list__check" />
|
| 215 |
+
<div>
|
| 216 |
+
<strong>Review latest labs before signing</strong>
|
| 217 |
+
<p className="muted">Abnormal values are highlighted in chart review.</p>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
<div className="task-list__item">
|
| 221 |
+
<span className="task-list__check" />
|
| 222 |
+
<div>
|
| 223 |
+
<strong>Complete progress note and order entry</strong>
|
| 224 |
+
<p className="muted">Orders can remain draft or move directly to signature.</p>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
</SectionCard>
|
| 229 |
+
</aside>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
</main>
|
| 233 |
+
);
|
| 234 |
+
}
|
apps/ehr/app/patient/[id]/actions.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use server";
|
| 2 |
+
|
| 3 |
+
import { randomUUID } from "node:crypto";
|
| 4 |
+
|
| 5 |
+
import { revalidatePath } from "next/cache";
|
| 6 |
+
|
| 7 |
+
import { prisma } from "../../../lib/db";
|
| 8 |
+
|
| 9 |
+
type OrderCategoryValue = "LAB" | "MED" | "IMAGING";
|
| 10 |
+
type OrderStatusValue = "DRAFT" | "PENDING_SIGNATURE" | "SIGNED";
|
| 11 |
+
|
| 12 |
+
function getRequiredField(formData: FormData, key: string) {
|
| 13 |
+
const value = formData.get(key);
|
| 14 |
+
if (!value || typeof value !== "string") {
|
| 15 |
+
throw new Error(`Missing field: ${key}`);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
return value.trim();
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function createProgressNoteAction(formData: FormData) {
|
| 22 |
+
const patientId = getRequiredField(formData, "patientId");
|
| 23 |
+
const encounterId = getRequiredField(formData, "encounterId");
|
| 24 |
+
const author = getRequiredField(formData, "author");
|
| 25 |
+
const title = getRequiredField(formData, "title");
|
| 26 |
+
const content = getRequiredField(formData, "content");
|
| 27 |
+
|
| 28 |
+
await prisma.clinicalNote.create({
|
| 29 |
+
data: {
|
| 30 |
+
id: randomUUID(),
|
| 31 |
+
encounterId,
|
| 32 |
+
type: "PROGRESS",
|
| 33 |
+
title,
|
| 34 |
+
author,
|
| 35 |
+
content,
|
| 36 |
+
signed: false
|
| 37 |
+
}
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
revalidatePath(`/patient/${patientId}`);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export async function createOrderAction(formData: FormData) {
|
| 44 |
+
const patientId = getRequiredField(formData, "patientId");
|
| 45 |
+
const encounterId = getRequiredField(formData, "encounterId");
|
| 46 |
+
const name = getRequiredField(formData, "name");
|
| 47 |
+
const category = getRequiredField(formData, "category") as OrderCategoryValue;
|
| 48 |
+
const parameters = getRequiredField(formData, "parameters");
|
| 49 |
+
const rationale = getRequiredField(formData, "rationale");
|
| 50 |
+
const submitForSignature = formData.get("submitForSignature") === "on";
|
| 51 |
+
const status: OrderStatusValue = submitForSignature ? "PENDING_SIGNATURE" : "DRAFT";
|
| 52 |
+
|
| 53 |
+
await prisma.order.create({
|
| 54 |
+
data: {
|
| 55 |
+
id: randomUUID(),
|
| 56 |
+
encounterId,
|
| 57 |
+
name,
|
| 58 |
+
category,
|
| 59 |
+
parametersJson: JSON.stringify({ freeText: parameters }),
|
| 60 |
+
rationale,
|
| 61 |
+
status
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
revalidatePath(`/patient/${patientId}`);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export async function signOrderAction(formData: FormData) {
|
| 69 |
+
const patientId = getRequiredField(formData, "patientId");
|
| 70 |
+
const orderId = getRequiredField(formData, "orderId");
|
| 71 |
+
|
| 72 |
+
await prisma.order.update({
|
| 73 |
+
where: { id: orderId },
|
| 74 |
+
data: { status: "SIGNED" }
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
revalidatePath(`/patient/${patientId}`);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export async function signEncounterAction(formData: FormData) {
|
| 81 |
+
const patientId = getRequiredField(formData, "patientId");
|
| 82 |
+
const encounterId = getRequiredField(formData, "encounterId");
|
| 83 |
+
|
| 84 |
+
await prisma.encounter.update({
|
| 85 |
+
where: { id: encounterId },
|
| 86 |
+
data: { status: "SIGNED" }
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
await prisma.clinicalNote.updateMany({
|
| 90 |
+
where: { encounterId },
|
| 91 |
+
data: { signed: true }
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
await prisma.order.updateMany({
|
| 95 |
+
where: {
|
| 96 |
+
encounterId,
|
| 97 |
+
status: {
|
| 98 |
+
in: ["DRAFT", "PENDING_SIGNATURE"]
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
data: { status: "SIGNED" }
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
revalidatePath(`/patient/${patientId}`);
|
| 105 |
+
}
|
apps/ehr/app/patient/[id]/page.tsx
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link";
|
| 2 |
+
import { notFound } from "next/navigation";
|
| 3 |
+
|
| 4 |
+
import { ActivityNav } from "../../../components/activity-nav";
|
| 5 |
+
import { AppBrand } from "../../../components/app-brand";
|
| 6 |
+
import { ChartReviewTabs } from "../../../components/chart-review-tabs";
|
| 7 |
+
import { SectionCard } from "../../../components/section-card";
|
| 8 |
+
import { WorkspaceSidebar } from "../../../components/workspace-sidebar";
|
| 9 |
+
import { formatDateTime, parseJsonValue } from "../../../lib/chart";
|
| 10 |
+
import { prisma } from "../../../lib/db";
|
| 11 |
+
import {
|
| 12 |
+
createOrderAction,
|
| 13 |
+
createProgressNoteAction,
|
| 14 |
+
signEncounterAction,
|
| 15 |
+
signOrderAction
|
| 16 |
+
} from "./actions";
|
| 17 |
+
|
| 18 |
+
type PatientPageData = {
|
| 19 |
+
id: string;
|
| 20 |
+
mrn: string;
|
| 21 |
+
fullName: string;
|
| 22 |
+
age: number;
|
| 23 |
+
sex: string;
|
| 24 |
+
allergiesJson: string;
|
| 25 |
+
bannerFlagsJson: string;
|
| 26 |
+
summary: string;
|
| 27 |
+
encounters: Array<{
|
| 28 |
+
id: string;
|
| 29 |
+
type: string;
|
| 30 |
+
reasonForVisit: string;
|
| 31 |
+
provider: string;
|
| 32 |
+
startedAt: Date;
|
| 33 |
+
status: string;
|
| 34 |
+
labs: Array<{
|
| 35 |
+
id: string;
|
| 36 |
+
name: string;
|
| 37 |
+
loinc: string | null;
|
| 38 |
+
value: string;
|
| 39 |
+
unit: string;
|
| 40 |
+
referenceRange: string;
|
| 41 |
+
abnormal: boolean;
|
| 42 |
+
collectedAt: Date;
|
| 43 |
+
}>;
|
| 44 |
+
notes: Array<{
|
| 45 |
+
id: string;
|
| 46 |
+
type: string;
|
| 47 |
+
title: string;
|
| 48 |
+
author: string;
|
| 49 |
+
content: string;
|
| 50 |
+
signed: boolean;
|
| 51 |
+
createdAt: Date;
|
| 52 |
+
}>;
|
| 53 |
+
orders: Array<{
|
| 54 |
+
id: string;
|
| 55 |
+
name: string;
|
| 56 |
+
category: string;
|
| 57 |
+
parametersJson: string;
|
| 58 |
+
status: string;
|
| 59 |
+
rationale: string;
|
| 60 |
+
createdAt: Date;
|
| 61 |
+
}>;
|
| 62 |
+
}>;
|
| 63 |
+
scenarios: Array<{
|
| 64 |
+
id: string;
|
| 65 |
+
encounterId: string;
|
| 66 |
+
title: string;
|
| 67 |
+
objective: string;
|
| 68 |
+
rubricJson: string;
|
| 69 |
+
requiredOrdersJson: string;
|
| 70 |
+
requiredNoteElementsJson: string;
|
| 71 |
+
}>;
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
type PatientPageProps = {
|
| 75 |
+
params: Promise<{
|
| 76 |
+
id: string;
|
| 77 |
+
}>;
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
export default async function PatientPage({ params }: PatientPageProps) {
|
| 81 |
+
const { id } = await params;
|
| 82 |
+
|
| 83 |
+
const patient: PatientPageData | null = await prisma.patient.findUnique({
|
| 84 |
+
where: { id },
|
| 85 |
+
include: {
|
| 86 |
+
encounters: {
|
| 87 |
+
orderBy: { startedAt: "desc" },
|
| 88 |
+
include: {
|
| 89 |
+
labs: {
|
| 90 |
+
orderBy: { collectedAt: "desc" }
|
| 91 |
+
},
|
| 92 |
+
notes: {
|
| 93 |
+
orderBy: { createdAt: "desc" }
|
| 94 |
+
},
|
| 95 |
+
orders: {
|
| 96 |
+
orderBy: { createdAt: "desc" }
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
scenarios: {
|
| 101 |
+
orderBy: { createdAt: "desc" }
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
if (!patient) {
|
| 107 |
+
notFound();
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
const activeEncounter = patient.encounters[0];
|
| 111 |
+
const scenario =
|
| 112 |
+
patient.scenarios.find((candidate: PatientPageData["scenarios"][number]) => candidate.encounterId === activeEncounter?.id) ??
|
| 113 |
+
patient.scenarios[0];
|
| 114 |
+
|
| 115 |
+
if (!activeEncounter) {
|
| 116 |
+
notFound();
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
const allergies = parseJsonValue<string[]>(patient.allergiesJson);
|
| 120 |
+
const flags = parseJsonValue<string[]>(patient.bannerFlagsJson);
|
| 121 |
+
const rubric = scenario ? parseJsonValue<string[]>(scenario.rubricJson) : [];
|
| 122 |
+
const requiredOrders = scenario ? parseJsonValue<string[]>(scenario.requiredOrdersJson) : [];
|
| 123 |
+
const requiredNoteElements = scenario ? parseJsonValue<string[]>(scenario.requiredNoteElementsJson) : [];
|
| 124 |
+
const problemList = Array.from(new Set([activeEncounter.reasonForVisit, ...flags]));
|
| 125 |
+
const visitDiagnoses = Array.from(new Set([scenario?.title ?? activeEncounter.reasonForVisit, activeEncounter.type, patient.summary]));
|
| 126 |
+
const globalNavItems = [
|
| 127 |
+
{ label: "Chart Review", href: "#chart-review" },
|
| 128 |
+
{ label: "Synopsis", href: "#summary" },
|
| 129 |
+
{ label: "Orders", href: "#orders" },
|
| 130 |
+
{ label: "Notes", href: "#notes" },
|
| 131 |
+
{ label: "Plan", href: "#summary" },
|
| 132 |
+
{ label: "Wrap-Up", href: "#encounter" }
|
| 133 |
+
];
|
| 134 |
+
const activityNavItems = [
|
| 135 |
+
{ label: "Summary", href: "#summary", testId: "activity-summary" },
|
| 136 |
+
{ label: "Chart Review", href: "#chart-review", testId: "activity-chart-review" },
|
| 137 |
+
{ label: "Notes", href: "#notes", testId: "activity-notes" },
|
| 138 |
+
{ label: "Orders", href: "#orders", testId: "activity-orders" },
|
| 139 |
+
{ label: "Wrap-Up", href: "#encounter", testId: "activity-encounter" }
|
| 140 |
+
];
|
| 141 |
+
const sidebarNavItems = [
|
| 142 |
+
{ label: "Summary", href: "#summary" },
|
| 143 |
+
{ label: "Labs & encounters", href: "#chart-review" },
|
| 144 |
+
{ label: "Documentation", href: "#notes" },
|
| 145 |
+
{ label: "Order Entry", href: "#orders" },
|
| 146 |
+
{ label: "Sign / close", href: "#encounter" }
|
| 147 |
+
];
|
| 148 |
+
|
| 149 |
+
return (
|
| 150 |
+
<main className="dashboard-shell">
|
| 151 |
+
<WorkspaceSidebar
|
| 152 |
+
brand={<AppBrand title="EHRGym" subtitle="Chart workspace" href="/" />}
|
| 153 |
+
sections={[
|
| 154 |
+
{
|
| 155 |
+
title: "Navigation",
|
| 156 |
+
items: [
|
| 157 |
+
{ label: "Dashboard", icon: "dashboard", href: "/" },
|
| 158 |
+
{ label: "Chart", icon: "chart", href: "#summary" }
|
| 159 |
+
]
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
title: "Sections",
|
| 163 |
+
items: [
|
| 164 |
+
{ label: "Summary", icon: "summary", href: "#summary" },
|
| 165 |
+
{ label: "Review", icon: "review", href: "#chart-review" },
|
| 166 |
+
{ label: "Orders", icon: "orders", href: "#orders" },
|
| 167 |
+
{ label: "Notes", icon: "notes", href: "#notes" }
|
| 168 |
+
]
|
| 169 |
+
}
|
| 170 |
+
]}
|
| 171 |
+
footerTitle="Active Visit"
|
| 172 |
+
footerText={`${activeEncounter.reasonForVisit} · ${activeEncounter.provider}`}
|
| 173 |
+
footerAction="Return to Worklist"
|
| 174 |
+
footerHref="/"
|
| 175 |
+
/>
|
| 176 |
+
|
| 177 |
+
<div className="dashboard-main">
|
| 178 |
+
<header className="workspace-header workspace-header--chart">
|
| 179 |
+
<div className="workspace-header__breadcrumbs">
|
| 180 |
+
<Link href="/" className="workspace-backlink">
|
| 181 |
+
← All patients
|
| 182 |
+
</Link>
|
| 183 |
+
<ActivityNav items={globalNavItems} className="workspace-pills" ariaLabel="Chart navigation" defaultHref="#chart-review" />
|
| 184 |
+
</div>
|
| 185 |
+
<div className="workspace-header__actions">
|
| 186 |
+
<a href="#encounter" className="workspace-toggle">
|
| 187 |
+
{activeEncounter.status}
|
| 188 |
+
</a>
|
| 189 |
+
<div className="workspace-profile workspace-profile--compact">
|
| 190 |
+
<div className="workspace-profile__avatar">{patient.fullName.slice(0, 1)}</div>
|
| 191 |
+
<div>
|
| 192 |
+
<strong>{activeEncounter.provider}</strong>
|
| 193 |
+
<p>{formatDateTime(activeEncounter.startedAt)}</p>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
</header>
|
| 198 |
+
|
| 199 |
+
<section className="patient-hero" data-testid="patient-banner">
|
| 200 |
+
<div className="patient-hero__identity">
|
| 201 |
+
<div>
|
| 202 |
+
<p className="patient-hero__eyebrow">Active Chart · MRN {patient.mrn}</p>
|
| 203 |
+
<h1 className="patient-hero__title">{patient.fullName}</h1>
|
| 204 |
+
<p className="patient-hero__lede">
|
| 205 |
+
{patient.age} y/o {patient.sex} · {activeEncounter.reasonForVisit}
|
| 206 |
+
</p>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
<div className="patient-hero__meta">
|
| 210 |
+
<div className="patient-hero__panel" data-testid="allergies-card">
|
| 211 |
+
<strong>Allergies</strong>
|
| 212 |
+
<span>{allergies.join(", ")}</span>
|
| 213 |
+
</div>
|
| 214 |
+
<div className="patient-hero__panel" data-testid="flags-card">
|
| 215 |
+
<strong>Chart flags</strong>
|
| 216 |
+
<span>{flags.join(" · ")}</span>
|
| 217 |
+
</div>
|
| 218 |
+
<div className="patient-hero__panel" data-testid="encounter-card">
|
| 219 |
+
<strong>Visit focus</strong>
|
| 220 |
+
<span>{scenario?.title ?? activeEncounter.type}</span>
|
| 221 |
+
</div>
|
| 222 |
+
<div className="patient-hero__panel patient-hero__panel--status">
|
| 223 |
+
<strong>Status</strong>
|
| 224 |
+
<span className="status-pill" data-status={activeEncounter.status}>
|
| 225 |
+
{activeEncounter.status}
|
| 226 |
+
</span>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
</section>
|
| 230 |
+
|
| 231 |
+
<ActivityNav items={activityNavItems} className="chart-activity-bar workspace-pills workspace-pills--wide" ariaLabel="Activity navigation" defaultHref="#summary" />
|
| 232 |
+
|
| 233 |
+
<section className="metric-grid metric-grid--chart">
|
| 234 |
+
<SectionCard title="Visit Goal" subtitle="Immediate objective" testId="scenario-brief" className="metric-card metric-card--wide">
|
| 235 |
+
<div id="summary" className="metric-card__stack">
|
| 236 |
+
<strong>{scenario?.objective ?? "Continue chart review and complete documentation."}</strong>
|
| 237 |
+
<p>{patient.summary}</p>
|
| 238 |
+
</div>
|
| 239 |
+
</SectionCard>
|
| 240 |
+
<SectionCard title="Pending Orders" subtitle="Expected for this visit" className="metric-card">
|
| 241 |
+
<div className="metric-card__value">{requiredOrders.length}</div>
|
| 242 |
+
<p>{requiredOrders.slice(0, 2).join(" · ") || "No required orders"}</p>
|
| 243 |
+
</SectionCard>
|
| 244 |
+
<SectionCard title="Documentation" subtitle="Expected note elements" className="metric-card">
|
| 245 |
+
<div className="metric-card__value">{requiredNoteElements.length}</div>
|
| 246 |
+
<p>{requiredNoteElements[0] ?? "Note ready for completion"}</p>
|
| 247 |
+
</SectionCard>
|
| 248 |
+
</section>
|
| 249 |
+
|
| 250 |
+
<div className="content-grid content-grid--chart">
|
| 251 |
+
<div className="content-stack">
|
| 252 |
+
<SectionCard title="Chart Review" subtitle="Encounter timeline and laboratory review" testId="chart-review-panel" className="section-card--chart">
|
| 253 |
+
<div id="chart-review">
|
| 254 |
+
<ChartReviewTabs encounters={patient.encounters} labs={activeEncounter.labs} notes={activeEncounter.notes} />
|
| 255 |
+
</div>
|
| 256 |
+
</SectionCard>
|
| 257 |
+
|
| 258 |
+
<SectionCard title="Notes" subtitle="Progress and clinical documentation" testId="notes-panel" className="section-card--notes">
|
| 259 |
+
<div id="notes" className="grid grid--2">
|
| 260 |
+
<form action={createProgressNoteAction} className="list-row" data-testid="note-form">
|
| 261 |
+
<input type="hidden" name="patientId" value={patient.id} />
|
| 262 |
+
<input type="hidden" name="encounterId" value={activeEncounter.id} />
|
| 263 |
+
<div className="form-grid">
|
| 264 |
+
<label className="field">
|
| 265 |
+
<span className="muted">Author</span>
|
| 266 |
+
<input aria-label="Note author" name="author" defaultValue="Resident Physician" required />
|
| 267 |
+
</label>
|
| 268 |
+
<label className="field">
|
| 269 |
+
<span className="muted">Title</span>
|
| 270 |
+
<input aria-label="Note title" name="title" defaultValue="Progress Note" required />
|
| 271 |
+
</label>
|
| 272 |
+
<label className="field">
|
| 273 |
+
<span className="muted">Progress note</span>
|
| 274 |
+
<textarea
|
| 275 |
+
aria-label="Progress note content"
|
| 276 |
+
name="content"
|
| 277 |
+
defaultValue={`S: \nO: Reviewed interval history and latest results.\nA: ${scenario?.title ?? activeEncounter.reasonForVisit}.\nP: `}
|
| 278 |
+
required
|
| 279 |
+
/>
|
| 280 |
+
</label>
|
| 281 |
+
<div className="form-actions">
|
| 282 |
+
<button className="primary-button" type="submit" data-testid="save-note-button">
|
| 283 |
+
File progress note
|
| 284 |
+
</button>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
</form>
|
| 288 |
+
|
| 289 |
+
<div className="note-list">
|
| 290 |
+
{activeEncounter.notes.map((note: PatientPageData["encounters"][number]["notes"][number]) => (
|
| 291 |
+
<article key={note.id} className="note-row" data-testid={`note-row-${note.id}`}>
|
| 292 |
+
<header>
|
| 293 |
+
<div>
|
| 294 |
+
<h3>{note.title}</h3>
|
| 295 |
+
<p className="muted">
|
| 296 |
+
{note.type} · {note.author} · {formatDateTime(note.createdAt)}
|
| 297 |
+
</p>
|
| 298 |
+
</div>
|
| 299 |
+
<span className="status-pill" data-status={note.signed ? "SIGNED" : "OPEN"}>
|
| 300 |
+
{note.signed ? "SIGNED" : "DRAFT"}
|
| 301 |
+
</span>
|
| 302 |
+
</header>
|
| 303 |
+
<p style={{ whiteSpace: "pre-wrap" }}>{note.content}</p>
|
| 304 |
+
</article>
|
| 305 |
+
))}
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
</SectionCard>
|
| 309 |
+
|
| 310 |
+
<SectionCard title="Orders" subtitle="Medication, lab, and imaging entry" testId="orders-panel" className="section-card--orders">
|
| 311 |
+
<div id="orders" className="grid grid--2">
|
| 312 |
+
<form action={createOrderAction} className="list-row" data-testid="order-form">
|
| 313 |
+
<input type="hidden" name="patientId" value={patient.id} />
|
| 314 |
+
<input type="hidden" name="encounterId" value={activeEncounter.id} />
|
| 315 |
+
<div className="form-grid">
|
| 316 |
+
<label className="field">
|
| 317 |
+
<span className="muted">Order name</span>
|
| 318 |
+
<input aria-label="Order name" name="name" placeholder="Normal saline bolus" required />
|
| 319 |
+
</label>
|
| 320 |
+
<label className="field">
|
| 321 |
+
<span className="muted">Category</span>
|
| 322 |
+
<select aria-label="Order category" name="category" defaultValue="LAB">
|
| 323 |
+
<option value="LAB">Lab</option>
|
| 324 |
+
<option value="MED">Medication</option>
|
| 325 |
+
<option value="IMAGING">Imaging</option>
|
| 326 |
+
</select>
|
| 327 |
+
</label>
|
| 328 |
+
<label className="field">
|
| 329 |
+
<span className="muted">Parameters</span>
|
| 330 |
+
<input aria-label="Order parameters" name="parameters" placeholder="1 L IV once" required />
|
| 331 |
+
</label>
|
| 332 |
+
<label className="field">
|
| 333 |
+
<span className="muted">Rationale</span>
|
| 334 |
+
<textarea aria-label="Order rationale" name="rationale" placeholder="Why is this order needed?" required />
|
| 335 |
+
</label>
|
| 336 |
+
<label className="checkbox-field">
|
| 337 |
+
<input className="checkbox-field__control" aria-label="Submit order for signature" name="submitForSignature" type="checkbox" />
|
| 338 |
+
<span>Send directly for signature</span>
|
| 339 |
+
</label>
|
| 340 |
+
<div className="form-actions">
|
| 341 |
+
<button className="primary-button" type="submit" data-testid="save-order-button">
|
| 342 |
+
Accept order
|
| 343 |
+
</button>
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
</form>
|
| 347 |
+
|
| 348 |
+
<div className="order-list">
|
| 349 |
+
{activeEncounter.orders.map((order: PatientPageData["encounters"][number]["orders"][number]) => {
|
| 350 |
+
const parameters = parseJsonValue<Record<string, string>>(order.parametersJson);
|
| 351 |
+
|
| 352 |
+
return (
|
| 353 |
+
<article key={order.id} className="order-row" data-testid={`order-row-${order.id}`}>
|
| 354 |
+
<header>
|
| 355 |
+
<div>
|
| 356 |
+
<h3>{order.name}</h3>
|
| 357 |
+
<p className="muted">
|
| 358 |
+
{order.category} · {formatDateTime(order.createdAt)}
|
| 359 |
+
</p>
|
| 360 |
+
</div>
|
| 361 |
+
<span className="status-pill" data-status={order.status}>
|
| 362 |
+
{order.status}
|
| 363 |
+
</span>
|
| 364 |
+
</header>
|
| 365 |
+
<p>
|
| 366 |
+
<strong>Parameters:</strong> {Object.values(parameters).join(", ")}
|
| 367 |
+
</p>
|
| 368 |
+
<p>
|
| 369 |
+
<strong>Rationale:</strong> {order.rationale}
|
| 370 |
+
</p>
|
| 371 |
+
{order.status !== "SIGNED" ? (
|
| 372 |
+
<form action={signOrderAction} className="form-actions">
|
| 373 |
+
<input type="hidden" name="patientId" value={patient.id} />
|
| 374 |
+
<input type="hidden" name="orderId" value={order.id} />
|
| 375 |
+
<button className="secondary-button" type="submit" data-testid={`sign-order-${order.id}`}>
|
| 376 |
+
Sign order
|
| 377 |
+
</button>
|
| 378 |
+
</form>
|
| 379 |
+
) : null}
|
| 380 |
+
</article>
|
| 381 |
+
);
|
| 382 |
+
})}
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
</SectionCard>
|
| 386 |
+
|
| 387 |
+
<SectionCard title="Wrap-Up" subtitle="Finalize documentation and orders" testId="encounter-panel" className="section-card--wrapup">
|
| 388 |
+
<div id="encounter" className="list-row list-row--split">
|
| 389 |
+
<p>Signing this visit finalizes notes and promotes all remaining draft or pending orders to signed.</p>
|
| 390 |
+
<form action={signEncounterAction}>
|
| 391 |
+
<input type="hidden" name="patientId" value={patient.id} />
|
| 392 |
+
<input type="hidden" name="encounterId" value={activeEncounter.id} />
|
| 393 |
+
<div className="form-actions">
|
| 394 |
+
<button className="primary-button" type="submit" data-testid="sign-encounter-button">
|
| 395 |
+
Sign visit
|
| 396 |
+
</button>
|
| 397 |
+
</div>
|
| 398 |
+
</form>
|
| 399 |
+
</div>
|
| 400 |
+
</SectionCard>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
+
<aside className="content-stack">
|
| 404 |
+
<SectionCard title="Review Status" subtitle="Visit workflow" className="section-card--summary">
|
| 405 |
+
<div className="rail-list">
|
| 406 |
+
{rubric.map((item) => (
|
| 407 |
+
<div key={item} className="rail-list__item">
|
| 408 |
+
<strong>{item}</strong>
|
| 409 |
+
<span>Pending review</span>
|
| 410 |
+
</div>
|
| 411 |
+
))}
|
| 412 |
+
</div>
|
| 413 |
+
</SectionCard>
|
| 414 |
+
|
| 415 |
+
<SectionCard title="Chart Navigation" subtitle="Common activities" className="section-card--summary">
|
| 416 |
+
<ActivityNav items={sidebarNavItems} className="sidebar-nav" ariaLabel="Chart sections" defaultHref="#summary" />
|
| 417 |
+
</SectionCard>
|
| 418 |
+
|
| 419 |
+
<SectionCard title="Problem List" subtitle="Active charted issues" className="section-card--problem-list">
|
| 420 |
+
<div className="problem-list">
|
| 421 |
+
{problemList.map((problem) => (
|
| 422 |
+
<div key={problem} className="problem-list__item">
|
| 423 |
+
<span>{problem}</span>
|
| 424 |
+
<span className="problem-list__status">Active</span>
|
| 425 |
+
</div>
|
| 426 |
+
))}
|
| 427 |
+
</div>
|
| 428 |
+
</SectionCard>
|
| 429 |
+
|
| 430 |
+
<SectionCard title="Visit Diagnoses" subtitle="Current encounter associations" className="section-card--diagnosis-list">
|
| 431 |
+
<div className="problem-list">
|
| 432 |
+
{visitDiagnoses.map((diagnosis) => (
|
| 433 |
+
<div key={diagnosis} className="problem-list__item">
|
| 434 |
+
<span>{diagnosis}</span>
|
| 435 |
+
<span className="problem-list__status problem-list__status--muted">Visit</span>
|
| 436 |
+
</div>
|
| 437 |
+
))}
|
| 438 |
+
</div>
|
| 439 |
+
</SectionCard>
|
| 440 |
+
|
| 441 |
+
<SectionCard title="Orders to Review" subtitle="Expected for this visit" className="section-card--summary">
|
| 442 |
+
<div className="summary-flags">
|
| 443 |
+
{requiredOrders.map((item) => (
|
| 444 |
+
<span key={item} className="summary-flag summary-flag--accent">
|
| 445 |
+
{item}
|
| 446 |
+
</span>
|
| 447 |
+
))}
|
| 448 |
+
</div>
|
| 449 |
+
</SectionCard>
|
| 450 |
+
</aside>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
</main>
|
| 454 |
+
);
|
| 455 |
+
}
|
apps/ehr/components/activity-nav.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useMemo, useState } from "react";
|
| 4 |
+
|
| 5 |
+
type NavItem = {
|
| 6 |
+
label: string;
|
| 7 |
+
href: string;
|
| 8 |
+
testId?: string;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
type ActivityNavProps = {
|
| 12 |
+
items: NavItem[];
|
| 13 |
+
className?: string;
|
| 14 |
+
defaultHref?: string;
|
| 15 |
+
ariaLabel: string;
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
export function ActivityNav({ items, className, defaultHref, ariaLabel }: ActivityNavProps) {
|
| 19 |
+
const fallbackHref = useMemo(() => defaultHref ?? items[0]?.href ?? "#summary", [defaultHref, items]);
|
| 20 |
+
const [activeHref, setActiveHref] = useState(fallbackHref);
|
| 21 |
+
|
| 22 |
+
useEffect(() => {
|
| 23 |
+
const updateActiveHref = () => {
|
| 24 |
+
const currentHash = window.location.hash || fallbackHref;
|
| 25 |
+
setActiveHref(currentHash);
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
updateActiveHref();
|
| 29 |
+
window.addEventListener("hashchange", updateActiveHref);
|
| 30 |
+
return () => window.removeEventListener("hashchange", updateActiveHref);
|
| 31 |
+
}, [fallbackHref]);
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<nav className={className} aria-label={ariaLabel}>
|
| 35 |
+
{items.map((item) => (
|
| 36 |
+
<a
|
| 37 |
+
key={`${item.href}-${item.label}`}
|
| 38 |
+
href={item.href}
|
| 39 |
+
className={activeHref === item.href ? "nav-link is-active" : "nav-link"}
|
| 40 |
+
aria-current={activeHref === item.href ? "page" : undefined}
|
| 41 |
+
data-testid={item.testId}
|
| 42 |
+
onClick={() => setActiveHref(item.href)}
|
| 43 |
+
>
|
| 44 |
+
{item.label}
|
| 45 |
+
</a>
|
| 46 |
+
))}
|
| 47 |
+
</nav>
|
| 48 |
+
);
|
| 49 |
+
}
|
apps/ehr/components/app-brand.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Image from "next/image";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
import type { Route } from "next";
|
| 4 |
+
import ehrgymIcon from "../../../ehrgym_icon.png";
|
| 5 |
+
|
| 6 |
+
type AppBrandProps = {
|
| 7 |
+
title: string;
|
| 8 |
+
subtitle: string;
|
| 9 |
+
href?: Route;
|
| 10 |
+
compact?: boolean;
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
export function AppBrand({ title, subtitle, href = "/", compact = false }: AppBrandProps) {
|
| 14 |
+
const content = (
|
| 15 |
+
<>
|
| 16 |
+
<span className={compact ? "app-brand__logo app-brand__logo--compact" : "app-brand__logo"} aria-hidden="true">
|
| 17 |
+
<Image src={ehrgymIcon} alt="" draggable={false} priority={compact ? false : true} />
|
| 18 |
+
</span>
|
| 19 |
+
<span className={compact ? "app-brand__copy app-brand__copy--compact" : "app-brand__copy"}>
|
| 20 |
+
<strong>{title}</strong>
|
| 21 |
+
<span>{subtitle}</span>
|
| 22 |
+
</span>
|
| 23 |
+
</>
|
| 24 |
+
);
|
| 25 |
+
|
| 26 |
+
if (!href) {
|
| 27 |
+
return <div className="app-brand">{content}</div>;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<Link href={href} className="app-brand" aria-label={title}>
|
| 32 |
+
{content}
|
| 33 |
+
</Link>
|
| 34 |
+
);
|
| 35 |
+
}
|
apps/ehr/components/chart-review-tabs.tsx
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useMemo, useState } from "react";
|
| 4 |
+
|
| 5 |
+
import { formatDateTime } from "../lib/chart";
|
| 6 |
+
|
| 7 |
+
type EncounterItem = {
|
| 8 |
+
id: string;
|
| 9 |
+
type: string;
|
| 10 |
+
reasonForVisit: string;
|
| 11 |
+
provider: string;
|
| 12 |
+
startedAt: Date | string;
|
| 13 |
+
status: string;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
type LabItem = {
|
| 17 |
+
id: string;
|
| 18 |
+
name: string;
|
| 19 |
+
loinc: string | null;
|
| 20 |
+
value: string;
|
| 21 |
+
unit: string;
|
| 22 |
+
referenceRange: string;
|
| 23 |
+
abnormal: boolean;
|
| 24 |
+
collectedAt: Date | string;
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
type NoteItem = {
|
| 28 |
+
id: string;
|
| 29 |
+
type: string;
|
| 30 |
+
title: string;
|
| 31 |
+
author: string;
|
| 32 |
+
content: string;
|
| 33 |
+
signed: boolean;
|
| 34 |
+
createdAt: Date | string;
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
type ChartReviewTabsProps = {
|
| 38 |
+
encounters: EncounterItem[];
|
| 39 |
+
labs: LabItem[];
|
| 40 |
+
notes: NoteItem[];
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
type TabKey = "encounters" | "labs" | "notes";
|
| 44 |
+
|
| 45 |
+
export function ChartReviewTabs({ encounters, labs, notes }: ChartReviewTabsProps) {
|
| 46 |
+
const [activeTab, setActiveTab] = useState<TabKey>("encounters");
|
| 47 |
+
|
| 48 |
+
const tabs = useMemo(
|
| 49 |
+
() => [
|
| 50 |
+
{ key: "encounters" as const, label: "Encounters" },
|
| 51 |
+
{ key: "labs" as const, label: "Labs" },
|
| 52 |
+
{ key: "notes" as const, label: "Clinical Notes" }
|
| 53 |
+
],
|
| 54 |
+
[]
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
return (
|
| 58 |
+
<div className="section-stack">
|
| 59 |
+
<div className="subtab-strip" aria-label="Chart review tabs" role="tablist">
|
| 60 |
+
{tabs.map((tab) => (
|
| 61 |
+
<button
|
| 62 |
+
key={tab.key}
|
| 63 |
+
type="button"
|
| 64 |
+
role="tab"
|
| 65 |
+
aria-selected={activeTab === tab.key}
|
| 66 |
+
aria-controls={`chart-tab-panel-${tab.key}`}
|
| 67 |
+
id={`chart-tab-${tab.key}`}
|
| 68 |
+
className={activeTab === tab.key ? "subtab-strip__item subtab-strip__item--active" : "subtab-strip__item"}
|
| 69 |
+
data-testid={`chart-tab-${tab.key}`}
|
| 70 |
+
onClick={() => setActiveTab(tab.key)}
|
| 71 |
+
>
|
| 72 |
+
{tab.label}
|
| 73 |
+
</button>
|
| 74 |
+
))}
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<section
|
| 78 |
+
id="chart-tab-panel-encounters"
|
| 79 |
+
role="tabpanel"
|
| 80 |
+
aria-labelledby="chart-tab-encounters"
|
| 81 |
+
hidden={activeTab !== "encounters"}
|
| 82 |
+
className="list-row"
|
| 83 |
+
data-testid="encounter-timeline"
|
| 84 |
+
>
|
| 85 |
+
<header>
|
| 86 |
+
<div>
|
| 87 |
+
<h3>Encounter timeline</h3>
|
| 88 |
+
<p className="muted">Linked visit history and responsible clinicians.</p>
|
| 89 |
+
</div>
|
| 90 |
+
</header>
|
| 91 |
+
<div className="timeline">
|
| 92 |
+
{encounters.map((encounter) => (
|
| 93 |
+
<div key={encounter.id} className="list-row">
|
| 94 |
+
<header>
|
| 95 |
+
<div>
|
| 96 |
+
<strong>{encounter.type}</strong>
|
| 97 |
+
<p className="muted">
|
| 98 |
+
{encounter.reasonForVisit} · {encounter.provider}
|
| 99 |
+
</p>
|
| 100 |
+
</div>
|
| 101 |
+
<span className="status-pill" data-status={encounter.status}>
|
| 102 |
+
{encounter.status}
|
| 103 |
+
</span>
|
| 104 |
+
</header>
|
| 105 |
+
<p className="muted">Started {formatDateTime(new Date(encounter.startedAt))}</p>
|
| 106 |
+
</div>
|
| 107 |
+
))}
|
| 108 |
+
</div>
|
| 109 |
+
</section>
|
| 110 |
+
|
| 111 |
+
<section
|
| 112 |
+
id="chart-tab-panel-labs"
|
| 113 |
+
role="tabpanel"
|
| 114 |
+
aria-labelledby="chart-tab-labs"
|
| 115 |
+
hidden={activeTab !== "labs"}
|
| 116 |
+
className="list-row"
|
| 117 |
+
data-testid="labs-table-wrapper"
|
| 118 |
+
>
|
| 119 |
+
<header>
|
| 120 |
+
<div>
|
| 121 |
+
<h3>Labs</h3>
|
| 122 |
+
<p className="muted">Recent resulted values for the active encounter.</p>
|
| 123 |
+
</div>
|
| 124 |
+
</header>
|
| 125 |
+
<table className="table" aria-label="Recent labs" data-testid="labs-table">
|
| 126 |
+
<thead>
|
| 127 |
+
<tr>
|
| 128 |
+
<th>Collected</th>
|
| 129 |
+
<th>Test</th>
|
| 130 |
+
<th>Value</th>
|
| 131 |
+
<th>Reference</th>
|
| 132 |
+
<th>LOINC</th>
|
| 133 |
+
</tr>
|
| 134 |
+
</thead>
|
| 135 |
+
<tbody>
|
| 136 |
+
{labs.map((lab) => (
|
| 137 |
+
<tr key={lab.id} className="lab-row" data-testid={`lab-row-${lab.id}`}>
|
| 138 |
+
<td>{formatDateTime(new Date(lab.collectedAt))}</td>
|
| 139 |
+
<td>{lab.name}</td>
|
| 140 |
+
<td className={lab.abnormal ? "abnormal" : undefined}>
|
| 141 |
+
{lab.value} {lab.unit}
|
| 142 |
+
</td>
|
| 143 |
+
<td>{lab.referenceRange}</td>
|
| 144 |
+
<td>{lab.loinc ?? "—"}</td>
|
| 145 |
+
</tr>
|
| 146 |
+
))}
|
| 147 |
+
</tbody>
|
| 148 |
+
</table>
|
| 149 |
+
</section>
|
| 150 |
+
|
| 151 |
+
<section
|
| 152 |
+
id="chart-tab-panel-notes"
|
| 153 |
+
role="tabpanel"
|
| 154 |
+
aria-labelledby="chart-tab-notes"
|
| 155 |
+
hidden={activeTab !== "notes"}
|
| 156 |
+
className="note-list"
|
| 157 |
+
data-testid="chart-review-notes"
|
| 158 |
+
>
|
| 159 |
+
{notes.map((note) => (
|
| 160 |
+
<article key={note.id} className="note-row" data-testid={`chart-note-row-${note.id}`}>
|
| 161 |
+
<header>
|
| 162 |
+
<div>
|
| 163 |
+
<h3>{note.title}</h3>
|
| 164 |
+
<p className="muted">
|
| 165 |
+
{note.type} · {note.author} · {formatDateTime(new Date(note.createdAt))}
|
| 166 |
+
</p>
|
| 167 |
+
</div>
|
| 168 |
+
<span className="status-pill" data-status={note.signed ? "SIGNED" : "OPEN"}>
|
| 169 |
+
{note.signed ? "SIGNED" : "DRAFT"}
|
| 170 |
+
</span>
|
| 171 |
+
</header>
|
| 172 |
+
<p style={{ whiteSpace: "pre-wrap" }}>{note.content}</p>
|
| 173 |
+
</article>
|
| 174 |
+
))}
|
| 175 |
+
</section>
|
| 176 |
+
</div>
|
| 177 |
+
);
|
| 178 |
+
}
|
apps/ehr/components/section-card.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode } from "react";
|
| 2 |
+
|
| 3 |
+
type SectionCardProps = {
|
| 4 |
+
title: string;
|
| 5 |
+
subtitle?: string;
|
| 6 |
+
children: ReactNode;
|
| 7 |
+
testId?: string;
|
| 8 |
+
className?: string;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export function SectionCard({ title, subtitle, children, testId, className }: SectionCardProps) {
|
| 12 |
+
return (
|
| 13 |
+
<section className={["section-card", className].filter(Boolean).join(" ")} data-testid={testId}>
|
| 14 |
+
<div className="section-card__header">
|
| 15 |
+
<div>
|
| 16 |
+
<h2>{title}</h2>
|
| 17 |
+
{subtitle ? <p>{subtitle}</p> : null}
|
| 18 |
+
</div>
|
| 19 |
+
</div>
|
| 20 |
+
{children}
|
| 21 |
+
</section>
|
| 22 |
+
);
|
| 23 |
+
}
|
apps/ehr/components/workspace-sidebar.tsx
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link";
|
| 2 |
+
import type { Route } from "next";
|
| 3 |
+
import type { ReactNode, SVGProps } from "react";
|
| 4 |
+
|
| 5 |
+
type SidebarIconName = "dashboard" | "patients" | "activity" | "snapshot" | "chart" | "summary" | "review" | "orders" | "notes";
|
| 6 |
+
|
| 7 |
+
type SidebarItem = {
|
| 8 |
+
label: string;
|
| 9 |
+
icon: SidebarIconName;
|
| 10 |
+
active?: boolean;
|
| 11 |
+
href?: string;
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
type SidebarSection = {
|
| 15 |
+
title: string;
|
| 16 |
+
items: SidebarItem[];
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
type WorkspaceSidebarProps = {
|
| 20 |
+
brand: ReactNode;
|
| 21 |
+
sections: SidebarSection[];
|
| 22 |
+
footerTitle: string;
|
| 23 |
+
footerText: string;
|
| 24 |
+
footerAction?: string;
|
| 25 |
+
footerHref?: string;
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
function SidebarGlyph({ name, ...props }: { name: SidebarIconName } & SVGProps<SVGSVGElement>) {
|
| 29 |
+
switch (name) {
|
| 30 |
+
case "dashboard":
|
| 31 |
+
return (
|
| 32 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" {...props}>
|
| 33 |
+
<rect x="3" y="3" width="5" height="5" rx="1" />
|
| 34 |
+
<rect x="12" y="3" width="5" height="5" rx="1" />
|
| 35 |
+
<rect x="3" y="12" width="5" height="5" rx="1" />
|
| 36 |
+
<rect x="12" y="12" width="5" height="5" rx="1" />
|
| 37 |
+
</svg>
|
| 38 |
+
);
|
| 39 |
+
case "patients":
|
| 40 |
+
return (
|
| 41 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" {...props}>
|
| 42 |
+
<path d="M5 5.5h10" />
|
| 43 |
+
<path d="M5 10h10" />
|
| 44 |
+
<path d="M5 14.5h10" />
|
| 45 |
+
<circle cx="3.5" cy="5.5" r="0.9" fill="currentColor" stroke="none" />
|
| 46 |
+
<circle cx="3.5" cy="10" r="0.9" fill="currentColor" stroke="none" />
|
| 47 |
+
<circle cx="3.5" cy="14.5" r="0.9" fill="currentColor" stroke="none" />
|
| 48 |
+
</svg>
|
| 49 |
+
);
|
| 50 |
+
case "activity":
|
| 51 |
+
return (
|
| 52 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
| 53 |
+
<path d="M3 12h3l2-4 3.2 7 2.1-4H17" />
|
| 54 |
+
</svg>
|
| 55 |
+
);
|
| 56 |
+
case "snapshot":
|
| 57 |
+
return (
|
| 58 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
| 59 |
+
<rect x="3" y="4" width="14" height="12" rx="2" />
|
| 60 |
+
<path d="M7 8h6" />
|
| 61 |
+
<path d="M7 12h4" />
|
| 62 |
+
</svg>
|
| 63 |
+
);
|
| 64 |
+
case "chart":
|
| 65 |
+
return (
|
| 66 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
| 67 |
+
<path d="M6 3.5h6l3 3V16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1Z" />
|
| 68 |
+
<path d="M12 3.5V7h3" />
|
| 69 |
+
</svg>
|
| 70 |
+
);
|
| 71 |
+
case "summary":
|
| 72 |
+
return (
|
| 73 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" {...props}>
|
| 74 |
+
<path d="M5 6h10" />
|
| 75 |
+
<path d="M5 10h10" />
|
| 76 |
+
<path d="M5 14h6" />
|
| 77 |
+
</svg>
|
| 78 |
+
);
|
| 79 |
+
case "review":
|
| 80 |
+
return (
|
| 81 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
| 82 |
+
<circle cx="9" cy="9" r="4.5" />
|
| 83 |
+
<path d="m13 13 3.5 3.5" />
|
| 84 |
+
</svg>
|
| 85 |
+
);
|
| 86 |
+
case "orders":
|
| 87 |
+
return (
|
| 88 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
| 89 |
+
<rect x="5" y="3.5" width="10" height="13" rx="1.5" />
|
| 90 |
+
<path d="M8 3.5h4" />
|
| 91 |
+
<path d="M7.5 8h5" />
|
| 92 |
+
<path d="M7.5 11h5" />
|
| 93 |
+
</svg>
|
| 94 |
+
);
|
| 95 |
+
case "notes":
|
| 96 |
+
return (
|
| 97 |
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
| 98 |
+
<path d="M6 3.5h8a1 1 0 0 1 1 1V16l-3-2-3 2-3-2-1 .7V4.5a1 1 0 0 1 1-1Z" />
|
| 99 |
+
<path d="M7.5 8h5" />
|
| 100 |
+
<path d="M7.5 10.8h5" />
|
| 101 |
+
</svg>
|
| 102 |
+
);
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
export function WorkspaceSidebar({ brand, sections, footerTitle, footerText, footerAction, footerHref }: WorkspaceSidebarProps) {
|
| 107 |
+
return (
|
| 108 |
+
<aside className="workspace-sidebar">
|
| 109 |
+
<div className="workspace-sidebar__brand">{brand}</div>
|
| 110 |
+
|
| 111 |
+
<div className="workspace-sidebar__sections">
|
| 112 |
+
{sections.map((section) => (
|
| 113 |
+
<section key={section.title} className="workspace-sidebar__section">
|
| 114 |
+
<p className="workspace-sidebar__heading">{section.title}</p>
|
| 115 |
+
<div className="workspace-sidebar__nav">
|
| 116 |
+
{section.items.map((item) => {
|
| 117 |
+
const className = item.active ? "workspace-sidebar__item workspace-sidebar__item--active" : "workspace-sidebar__item";
|
| 118 |
+
const content = (
|
| 119 |
+
<>
|
| 120 |
+
<span className="workspace-sidebar__icon" aria-hidden="true">
|
| 121 |
+
<SidebarGlyph name={item.icon} />
|
| 122 |
+
</span>
|
| 123 |
+
<span>{item.label}</span>
|
| 124 |
+
</>
|
| 125 |
+
);
|
| 126 |
+
|
| 127 |
+
if (item.href?.startsWith("#")) {
|
| 128 |
+
return (
|
| 129 |
+
<a key={item.label} href={item.href} className={className}>
|
| 130 |
+
{content}
|
| 131 |
+
</a>
|
| 132 |
+
);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
if (item.href) {
|
| 136 |
+
return (
|
| 137 |
+
<Link key={item.label} href={item.href as Route} className={className}>
|
| 138 |
+
{content}
|
| 139 |
+
</Link>
|
| 140 |
+
);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
return (
|
| 144 |
+
<button key={item.label} type="button" className={className}>
|
| 145 |
+
{content}
|
| 146 |
+
</button>
|
| 147 |
+
);
|
| 148 |
+
})}
|
| 149 |
+
</div>
|
| 150 |
+
</section>
|
| 151 |
+
))}
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<div className="workspace-sidebar__footer">
|
| 155 |
+
<strong>{footerTitle}</strong>
|
| 156 |
+
<p>{footerText}</p>
|
| 157 |
+
{footerAction
|
| 158 |
+
? footerHref?.startsWith("#")
|
| 159 |
+
? (
|
| 160 |
+
<a href={footerHref} className="secondary-button workspace-sidebar__footer-action">
|
| 161 |
+
{footerAction}
|
| 162 |
+
</a>
|
| 163 |
+
)
|
| 164 |
+
: footerHref
|
| 165 |
+
? (
|
| 166 |
+
<Link href={footerHref as Route} className="secondary-button workspace-sidebar__footer-action">
|
| 167 |
+
{footerAction}
|
| 168 |
+
</Link>
|
| 169 |
+
)
|
| 170 |
+
: (
|
| 171 |
+
<button type="button" className="secondary-button workspace-sidebar__footer-action">
|
| 172 |
+
{footerAction}
|
| 173 |
+
</button>
|
| 174 |
+
)
|
| 175 |
+
: null}
|
| 176 |
+
</div>
|
| 177 |
+
</aside>
|
| 178 |
+
);
|
| 179 |
+
}
|
apps/ehr/next-env.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="next" />
|
| 2 |
+
/// <reference types="next/image-types/global" />
|
| 3 |
+
/// <reference path="./.next/types/routes.d.ts" />
|
| 4 |
+
|
| 5 |
+
// NOTE: This file should not be edited
|
| 6 |
+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
apps/ehr/next.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
typedRoutes: true
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default nextConfig;
|
apps/ehr/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@ehrgym/ehr",
|
| 3 |
+
"private": true,
|
| 4 |
+
"scripts": {
|
| 5 |
+
"clean": "node -e \"require('fs').rmSync('.next', { recursive: true, force: true })\"",
|
| 6 |
+
"dev": "npm run clean && next dev --hostname 0.0.0.0 --port 3000",
|
| 7 |
+
"build": "npm run clean && next build",
|
| 8 |
+
"start": "next start --hostname 0.0.0.0 --port 3000",
|
| 9 |
+
"typecheck": "tsc --project tsconfig.json --noEmit"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@prisma/client": "^6.5.0",
|
| 13 |
+
"next": "^15.2.0",
|
| 14 |
+
"react": "^19.0.0",
|
| 15 |
+
"react-dom": "^19.0.0"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@types/react": "^19.0.10",
|
| 19 |
+
"@types/react-dom": "^19.0.4"
|
| 20 |
+
}
|
| 21 |
+
}
|
apps/ehr/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "../../tsconfig.base.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"plugins": [
|
| 5 |
+
{
|
| 6 |
+
"name": "next"
|
| 7 |
+
}
|
| 8 |
+
]
|
| 9 |
+
},
|
| 10 |
+
"include": [
|
| 11 |
+
"next-env.d.ts",
|
| 12 |
+
"**/*.ts",
|
| 13 |
+
"**/*.tsx",
|
| 14 |
+
".next/types/**/*.ts"
|
| 15 |
+
],
|
| 16 |
+
"exclude": ["node_modules"]
|
| 17 |
+
}
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.9"
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
ehrgym:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: docker/Dockerfile
|
| 8 |
+
environment:
|
| 9 |
+
DATABASE_URL: file:/app/prisma/dev.db
|
| 10 |
+
EHR_BASE_URL: http://127.0.0.1:3000
|
| 11 |
+
PORT: "3000"
|
| 12 |
+
PLAYWRIGHT_HEADLESS: "true"
|
| 13 |
+
OPENENV_DEFAULT_WAIT_MS: "350"
|
| 14 |
+
ports:
|
| 15 |
+
- "3000:3000"
|
| 16 |
+
- "8000:8000"
|
docker/Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-bookworm
|
| 2 |
+
|
| 3 |
+
RUN apt-get update \
|
| 4 |
+
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
|
| 5 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
COPY . /app
|
| 9 |
+
|
| 10 |
+
RUN npm install \
|
| 11 |
+
&& python3 -m pip install --no-cache-dir . \
|
| 12 |
+
&& python3 -m playwright install --with-deps chromium \
|
| 13 |
+
&& npx prisma generate \
|
| 14 |
+
&& npx prisma db push \
|
| 15 |
+
&& npx prisma db seed \
|
| 16 |
+
&& npm run build:ehr
|
| 17 |
+
|
| 18 |
+
EXPOSE 3000 8000
|
| 19 |
+
ENTRYPOINT ["./docker/entrypoint.sh"]
|
docker/entrypoint.sh
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
cleanup() {
|
| 5 |
+
if [[ -n "${ENV_SERVER_PID:-}" ]]; then
|
| 6 |
+
kill "$ENV_SERVER_PID" >/dev/null 2>&1 || true
|
| 7 |
+
fi
|
| 8 |
+
}
|
| 9 |
+
trap cleanup EXIT INT TERM
|
| 10 |
+
|
| 11 |
+
export DATABASE_URL="${DATABASE_URL:-file:/app/prisma/dev.db}"
|
| 12 |
+
export PORT="${PORT:-3000}"
|
| 13 |
+
export EHR_BASE_URL="${EHR_BASE_URL:-http://127.0.0.1:${PORT}}"
|
| 14 |
+
|
| 15 |
+
npx prisma generate
|
| 16 |
+
npx prisma db push
|
| 17 |
+
npx prisma db seed
|
| 18 |
+
|
| 19 |
+
uvicorn env_server.app.main:app --host 0.0.0.0 --port 8000 &
|
| 20 |
+
ENV_SERVER_PID=$!
|
| 21 |
+
|
| 22 |
+
npm run start --workspace @ehrgym/ehr -- --hostname 0.0.0.0 --port "$PORT"
|
env_server/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Environment server package for EHRGym."""
|
env_server/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI app for the EHRGym environment server."""
|
env_server/app/browser.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import base64
|
| 5 |
+
from typing import Any, Optional
|
| 6 |
+
from urllib.parse import urljoin
|
| 7 |
+
|
| 8 |
+
from playwright.async_api import Browser, Page, Playwright, async_playwright
|
| 9 |
+
|
| 10 |
+
from .models import Action, Observation
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class BrowserSession:
|
| 14 |
+
def __init__(self) -> None:
|
| 15 |
+
self._playwright: Optional[Playwright] = None
|
| 16 |
+
self._browser: Optional[Browser] = None
|
| 17 |
+
self.page: Optional[Page] = None
|
| 18 |
+
|
| 19 |
+
async def ensure_started(self, *, headless: bool) -> None:
|
| 20 |
+
if self._browser and self.page:
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
self._playwright = await async_playwright().start()
|
| 24 |
+
self._browser = await self._playwright.chromium.launch(headless=headless)
|
| 25 |
+
context = await self._browser.new_context(viewport={"width": 1440, "height": 1024})
|
| 26 |
+
self.page = await context.new_page()
|
| 27 |
+
|
| 28 |
+
async def close(self) -> None:
|
| 29 |
+
if self.page:
|
| 30 |
+
await self.page.context.close()
|
| 31 |
+
self.page = None
|
| 32 |
+
|
| 33 |
+
if self._browser:
|
| 34 |
+
await self._browser.close()
|
| 35 |
+
self._browser = None
|
| 36 |
+
|
| 37 |
+
if self._playwright:
|
| 38 |
+
await self._playwright.stop()
|
| 39 |
+
self._playwright = None
|
| 40 |
+
|
| 41 |
+
async def reset(self, base_url: str) -> None:
|
| 42 |
+
if not self.page:
|
| 43 |
+
raise RuntimeError("Browser session has not been started.")
|
| 44 |
+
|
| 45 |
+
await self.page.goto(base_url, wait_until="networkidle")
|
| 46 |
+
|
| 47 |
+
async def perform(self, action: Action, *, default_wait_ms: int) -> dict[str, Any]:
|
| 48 |
+
if not self.page:
|
| 49 |
+
raise RuntimeError("Browser session has not been started.")
|
| 50 |
+
|
| 51 |
+
metadata: dict[str, Any] = {"action_type": action.type, "success": True}
|
| 52 |
+
|
| 53 |
+
if action.type == "goto":
|
| 54 |
+
target_url = action.url or "/"
|
| 55 |
+
if not target_url.startswith("http"):
|
| 56 |
+
current_origin = self.page.url.split("/", 3)
|
| 57 |
+
base_origin = "/".join(current_origin[:3]) if len(current_origin) >= 3 else "http://127.0.0.1:3000"
|
| 58 |
+
target_url = urljoin(f"{base_origin}/", target_url.lstrip("/"))
|
| 59 |
+
await self.page.goto(target_url, wait_until="networkidle")
|
| 60 |
+
elif action.type == "click":
|
| 61 |
+
if not action.selector:
|
| 62 |
+
raise ValueError("click action requires selector")
|
| 63 |
+
await self.page.locator(action.selector).click()
|
| 64 |
+
elif action.type == "fill":
|
| 65 |
+
if not action.selector:
|
| 66 |
+
raise ValueError("fill action requires selector")
|
| 67 |
+
await self.page.locator(action.selector).fill(action.text or "")
|
| 68 |
+
elif action.type == "keypress":
|
| 69 |
+
if not action.key:
|
| 70 |
+
raise ValueError("keypress action requires key")
|
| 71 |
+
await self.page.keyboard.press(action.key)
|
| 72 |
+
elif action.type == "wait":
|
| 73 |
+
await asyncio.sleep((action.milliseconds or default_wait_ms) / 1000)
|
| 74 |
+
else:
|
| 75 |
+
metadata["success"] = False
|
| 76 |
+
metadata["error"] = f"Unsupported action: {action.type}"
|
| 77 |
+
|
| 78 |
+
return metadata
|
| 79 |
+
|
| 80 |
+
async def observe(self, *, goal: str, metadata: dict[str, Any]) -> Observation:
|
| 81 |
+
if not self.page:
|
| 82 |
+
raise RuntimeError("Browser session has not been started.")
|
| 83 |
+
|
| 84 |
+
screenshot = await self.page.screenshot(type="png", full_page=True)
|
| 85 |
+
screenshot_b64 = base64.b64encode(screenshot).decode("utf-8")
|
| 86 |
+
current_url = self.page.url
|
| 87 |
+
active_activity = "/" if current_url.endswith(":3000/") else current_url.rsplit("/", 1)[-1]
|
| 88 |
+
|
| 89 |
+
return Observation(
|
| 90 |
+
goal=goal,
|
| 91 |
+
screenshot_b64=screenshot_b64,
|
| 92 |
+
current_url=current_url,
|
| 93 |
+
active_activity=active_activity,
|
| 94 |
+
metadata=metadata,
|
| 95 |
+
)
|
env_server/app/main.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from contextlib import asynccontextmanager
|
| 5 |
+
from typing import Any, Optional
|
| 6 |
+
from uuid import uuid4
|
| 7 |
+
|
| 8 |
+
import httpx
|
| 9 |
+
from fastapi import FastAPI, HTTPException
|
| 10 |
+
|
| 11 |
+
from .browser import BrowserSession
|
| 12 |
+
from .models import Action, EnvironmentState, ResetRequest, ResetResponse, StepResponse
|
| 13 |
+
|
| 14 |
+
EHR_BASE_URL = os.getenv("EHR_BASE_URL", "http://127.0.0.1:3000")
|
| 15 |
+
HEADLESS = os.getenv("PLAYWRIGHT_HEADLESS", "true").lower() != "false"
|
| 16 |
+
DEFAULT_WAIT_MS = int(os.getenv("OPENENV_DEFAULT_WAIT_MS", "350"))
|
| 17 |
+
|
| 18 |
+
browser = BrowserSession()
|
| 19 |
+
state = EnvironmentState(episode_id="bootstrap")
|
| 20 |
+
goal_text = "Open the chart and complete the assigned workflow."
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
async def _post_reset() -> None:
|
| 24 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 25 |
+
response = await client.post(f"{EHR_BASE_URL}/api/dev/reset")
|
| 26 |
+
response.raise_for_status()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
async def _fetch_patients() -> list[dict[str, Any]]:
|
| 30 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 31 |
+
response = await client.get(f"{EHR_BASE_URL}/api/patients")
|
| 32 |
+
response.raise_for_status()
|
| 33 |
+
return response.json()["patients"]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
async def _fetch_patient(patient_id: str) -> dict[str, Any]:
|
| 37 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 38 |
+
response = await client.get(f"{EHR_BASE_URL}/api/patients/{patient_id}")
|
| 39 |
+
response.raise_for_status()
|
| 40 |
+
return response.json()["patient"]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
async def _refresh_progress() -> tuple[list[str], bool]:
|
| 44 |
+
if not state.patient_id:
|
| 45 |
+
return [], False
|
| 46 |
+
|
| 47 |
+
patient = await _fetch_patient(state.patient_id)
|
| 48 |
+
scenario = next((item for item in patient["scenarios"] if item["id"] == state.scenario_id), None)
|
| 49 |
+
encounter = next((item for item in patient["encounters"] if item["id"] == state.encounter_id), None)
|
| 50 |
+
|
| 51 |
+
if not scenario or not encounter:
|
| 52 |
+
return [], False
|
| 53 |
+
|
| 54 |
+
completed: list[str] = []
|
| 55 |
+
order_names = {order["name"] for order in encounter["orders"] if order["status"] == "SIGNED"}
|
| 56 |
+
if set(scenario["requiredOrders"]).issubset(order_names):
|
| 57 |
+
completed.append("required_orders")
|
| 58 |
+
|
| 59 |
+
note_text = "\n".join(note["content"] for note in encounter["notes"])
|
| 60 |
+
if all(element.lower() in note_text.lower() for element in scenario["requiredNoteElements"]):
|
| 61 |
+
completed.append("required_note_elements")
|
| 62 |
+
|
| 63 |
+
if encounter["status"] == "SIGNED":
|
| 64 |
+
completed.append("encounter_signed")
|
| 65 |
+
|
| 66 |
+
return completed, len(completed) == 3
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@asynccontextmanager
|
| 70 |
+
async def lifespan(_: FastAPI):
|
| 71 |
+
await browser.ensure_started(headless=HEADLESS)
|
| 72 |
+
yield
|
| 73 |
+
await browser.close()
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
app = FastAPI(title="EHRGym Environment Server", version="0.1.0", lifespan=lifespan)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@app.get("/healthz")
|
| 80 |
+
async def healthz() -> dict[str, str]:
|
| 81 |
+
return {"status": "ok"}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@app.post("/reset", response_model=ResetResponse)
|
| 85 |
+
async def reset(request: Optional[ResetRequest] = None) -> ResetResponse:
|
| 86 |
+
global state, goal_text
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
await _post_reset()
|
| 90 |
+
patients = await _fetch_patients()
|
| 91 |
+
except httpx.HTTPError as error:
|
| 92 |
+
raise HTTPException(status_code=502, detail=f"Failed to reset EHR app: {error}") from error
|
| 93 |
+
|
| 94 |
+
patient = next((item for item in patients if item["id"] == request.patient_id), None) if request else None
|
| 95 |
+
patient = patient or patients[0]
|
| 96 |
+
if not patient:
|
| 97 |
+
raise HTTPException(status_code=500, detail="No synthetic patients available after reset")
|
| 98 |
+
|
| 99 |
+
scenario = patient.get("scenario")
|
| 100 |
+
encounter = patient.get("encounter")
|
| 101 |
+
|
| 102 |
+
state = EnvironmentState(
|
| 103 |
+
episode_id=str(uuid4()),
|
| 104 |
+
patient_id=patient["id"],
|
| 105 |
+
encounter_id=encounter["id"] if encounter else None,
|
| 106 |
+
scenario_id=scenario["id"] if scenario else None,
|
| 107 |
+
rubric_progress=[],
|
| 108 |
+
cumulative_reward=0.0,
|
| 109 |
+
step_count=0,
|
| 110 |
+
)
|
| 111 |
+
goal_text = scenario["objective"] if scenario else "Open the chart and complete the assigned workflow."
|
| 112 |
+
|
| 113 |
+
await browser.reset(EHR_BASE_URL)
|
| 114 |
+
observation = await browser.observe(goal=goal_text, metadata={"reset": True})
|
| 115 |
+
return ResetResponse(observation=observation, state=state)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@app.post("/step", response_model=StepResponse)
|
| 119 |
+
async def step(action: Action) -> StepResponse:
|
| 120 |
+
global state
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
metadata = await browser.perform(action, default_wait_ms=DEFAULT_WAIT_MS)
|
| 124 |
+
except Exception as error: # noqa: BLE001
|
| 125 |
+
metadata = {"success": False, "error": str(error), "action_type": action.type}
|
| 126 |
+
|
| 127 |
+
state.step_count += 1
|
| 128 |
+
reward = 0.02 if metadata.get("success") else -0.05
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
rubric_progress, done = await _refresh_progress()
|
| 132 |
+
except httpx.HTTPError as error:
|
| 133 |
+
rubric_progress, done = [], False
|
| 134 |
+
metadata["progress_error"] = str(error)
|
| 135 |
+
|
| 136 |
+
if rubric_progress:
|
| 137 |
+
reward += 0.1 * len(rubric_progress)
|
| 138 |
+
|
| 139 |
+
state.rubric_progress = rubric_progress
|
| 140 |
+
state.cumulative_reward += reward
|
| 141 |
+
|
| 142 |
+
observation = await browser.observe(goal=goal_text, metadata=metadata)
|
| 143 |
+
return StepResponse(
|
| 144 |
+
observation=observation,
|
| 145 |
+
state=state,
|
| 146 |
+
reward=reward,
|
| 147 |
+
done=done,
|
| 148 |
+
info={"rubric_progress": rubric_progress},
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@app.get("/state", response_model=EnvironmentState)
|
| 153 |
+
async def get_state() -> EnvironmentState:
|
| 154 |
+
return state
|
env_server/app/models.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Literal, Optional
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
ActionType = Literal["goto", "click", "fill", "keypress", "wait"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Action(BaseModel):
|
| 12 |
+
type: ActionType
|
| 13 |
+
selector: Optional[str] = None
|
| 14 |
+
text: Optional[str] = None
|
| 15 |
+
url: Optional[str] = None
|
| 16 |
+
key: Optional[str] = None
|
| 17 |
+
milliseconds: Optional[int] = Field(default=None, ge=0)
|
| 18 |
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class Observation(BaseModel):
|
| 22 |
+
goal: str
|
| 23 |
+
screenshot_b64: str
|
| 24 |
+
current_url: str
|
| 25 |
+
active_activity: str
|
| 26 |
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class EnvironmentState(BaseModel):
|
| 30 |
+
episode_id: str
|
| 31 |
+
step_count: int = 0
|
| 32 |
+
patient_id: Optional[str] = None
|
| 33 |
+
encounter_id: Optional[str] = None
|
| 34 |
+
scenario_id: Optional[str] = None
|
| 35 |
+
rubric_progress: list[str] = Field(default_factory=list)
|
| 36 |
+
cumulative_reward: float = 0.0
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ResetRequest(BaseModel):
|
| 40 |
+
patient_id: Optional[str] = None
|
| 41 |
+
scenario_id: Optional[str] = None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ResetResponse(BaseModel):
|
| 45 |
+
observation: Observation
|
| 46 |
+
state: EnvironmentState
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class StepResponse(BaseModel):
|
| 50 |
+
observation: Observation
|
| 51 |
+
state: EnvironmentState
|
| 52 |
+
reward: float
|
| 53 |
+
done: bool
|
| 54 |
+
info: dict[str, Any] = Field(default_factory=dict)
|
package-lock.json
ADDED
|
@@ -0,0 +1,2249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "ehrgym",
|
| 3 |
+
"lockfileVersion": 3,
|
| 4 |
+
"requires": true,
|
| 5 |
+
"packages": {
|
| 6 |
+
"": {
|
| 7 |
+
"name": "ehrgym",
|
| 8 |
+
"workspaces": [
|
| 9 |
+
"apps/ehr"
|
| 10 |
+
],
|
| 11 |
+
"devDependencies": {
|
| 12 |
+
"@types/node": "^22.13.14",
|
| 13 |
+
"concurrently": "^9.1.2",
|
| 14 |
+
"prisma": "^6.5.0",
|
| 15 |
+
"tsx": "^4.19.3",
|
| 16 |
+
"typescript": "^5.8.2"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"apps/ehr": {
|
| 20 |
+
"name": "@ehrgym/ehr",
|
| 21 |
+
"dependencies": {
|
| 22 |
+
"@prisma/client": "^6.5.0",
|
| 23 |
+
"next": "^15.2.0",
|
| 24 |
+
"react": "^19.0.0",
|
| 25 |
+
"react-dom": "^19.0.0"
|
| 26 |
+
},
|
| 27 |
+
"devDependencies": {
|
| 28 |
+
"@types/react": "^19.0.10",
|
| 29 |
+
"@types/react-dom": "^19.0.4"
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"node_modules/@ehrgym/ehr": {
|
| 33 |
+
"resolved": "apps/ehr",
|
| 34 |
+
"link": true
|
| 35 |
+
},
|
| 36 |
+
"node_modules/@emnapi/runtime": {
|
| 37 |
+
"version": "1.8.1",
|
| 38 |
+
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
| 39 |
+
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
| 40 |
+
"license": "MIT",
|
| 41 |
+
"optional": true,
|
| 42 |
+
"dependencies": {
|
| 43 |
+
"tslib": "^2.4.0"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 47 |
+
"version": "0.27.3",
|
| 48 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
| 49 |
+
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
| 50 |
+
"cpu": [
|
| 51 |
+
"ppc64"
|
| 52 |
+
],
|
| 53 |
+
"dev": true,
|
| 54 |
+
"license": "MIT",
|
| 55 |
+
"optional": true,
|
| 56 |
+
"os": [
|
| 57 |
+
"aix"
|
| 58 |
+
],
|
| 59 |
+
"engines": {
|
| 60 |
+
"node": ">=18"
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
"node_modules/@esbuild/android-arm": {
|
| 64 |
+
"version": "0.27.3",
|
| 65 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
| 66 |
+
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
| 67 |
+
"cpu": [
|
| 68 |
+
"arm"
|
| 69 |
+
],
|
| 70 |
+
"dev": true,
|
| 71 |
+
"license": "MIT",
|
| 72 |
+
"optional": true,
|
| 73 |
+
"os": [
|
| 74 |
+
"android"
|
| 75 |
+
],
|
| 76 |
+
"engines": {
|
| 77 |
+
"node": ">=18"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"node_modules/@esbuild/android-arm64": {
|
| 81 |
+
"version": "0.27.3",
|
| 82 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
| 83 |
+
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
| 84 |
+
"cpu": [
|
| 85 |
+
"arm64"
|
| 86 |
+
],
|
| 87 |
+
"dev": true,
|
| 88 |
+
"license": "MIT",
|
| 89 |
+
"optional": true,
|
| 90 |
+
"os": [
|
| 91 |
+
"android"
|
| 92 |
+
],
|
| 93 |
+
"engines": {
|
| 94 |
+
"node": ">=18"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"node_modules/@esbuild/android-x64": {
|
| 98 |
+
"version": "0.27.3",
|
| 99 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
| 100 |
+
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
| 101 |
+
"cpu": [
|
| 102 |
+
"x64"
|
| 103 |
+
],
|
| 104 |
+
"dev": true,
|
| 105 |
+
"license": "MIT",
|
| 106 |
+
"optional": true,
|
| 107 |
+
"os": [
|
| 108 |
+
"android"
|
| 109 |
+
],
|
| 110 |
+
"engines": {
|
| 111 |
+
"node": ">=18"
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 115 |
+
"version": "0.27.3",
|
| 116 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
| 117 |
+
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
| 118 |
+
"cpu": [
|
| 119 |
+
"arm64"
|
| 120 |
+
],
|
| 121 |
+
"dev": true,
|
| 122 |
+
"license": "MIT",
|
| 123 |
+
"optional": true,
|
| 124 |
+
"os": [
|
| 125 |
+
"darwin"
|
| 126 |
+
],
|
| 127 |
+
"engines": {
|
| 128 |
+
"node": ">=18"
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 132 |
+
"version": "0.27.3",
|
| 133 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
| 134 |
+
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
| 135 |
+
"cpu": [
|
| 136 |
+
"x64"
|
| 137 |
+
],
|
| 138 |
+
"dev": true,
|
| 139 |
+
"license": "MIT",
|
| 140 |
+
"optional": true,
|
| 141 |
+
"os": [
|
| 142 |
+
"darwin"
|
| 143 |
+
],
|
| 144 |
+
"engines": {
|
| 145 |
+
"node": ">=18"
|
| 146 |
+
}
|
| 147 |
+
},
|
| 148 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 149 |
+
"version": "0.27.3",
|
| 150 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
| 151 |
+
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
| 152 |
+
"cpu": [
|
| 153 |
+
"arm64"
|
| 154 |
+
],
|
| 155 |
+
"dev": true,
|
| 156 |
+
"license": "MIT",
|
| 157 |
+
"optional": true,
|
| 158 |
+
"os": [
|
| 159 |
+
"freebsd"
|
| 160 |
+
],
|
| 161 |
+
"engines": {
|
| 162 |
+
"node": ">=18"
|
| 163 |
+
}
|
| 164 |
+
},
|
| 165 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 166 |
+
"version": "0.27.3",
|
| 167 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
| 168 |
+
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
| 169 |
+
"cpu": [
|
| 170 |
+
"x64"
|
| 171 |
+
],
|
| 172 |
+
"dev": true,
|
| 173 |
+
"license": "MIT",
|
| 174 |
+
"optional": true,
|
| 175 |
+
"os": [
|
| 176 |
+
"freebsd"
|
| 177 |
+
],
|
| 178 |
+
"engines": {
|
| 179 |
+
"node": ">=18"
|
| 180 |
+
}
|
| 181 |
+
},
|
| 182 |
+
"node_modules/@esbuild/linux-arm": {
|
| 183 |
+
"version": "0.27.3",
|
| 184 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
| 185 |
+
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
| 186 |
+
"cpu": [
|
| 187 |
+
"arm"
|
| 188 |
+
],
|
| 189 |
+
"dev": true,
|
| 190 |
+
"license": "MIT",
|
| 191 |
+
"optional": true,
|
| 192 |
+
"os": [
|
| 193 |
+
"linux"
|
| 194 |
+
],
|
| 195 |
+
"engines": {
|
| 196 |
+
"node": ">=18"
|
| 197 |
+
}
|
| 198 |
+
},
|
| 199 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 200 |
+
"version": "0.27.3",
|
| 201 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
| 202 |
+
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
| 203 |
+
"cpu": [
|
| 204 |
+
"arm64"
|
| 205 |
+
],
|
| 206 |
+
"dev": true,
|
| 207 |
+
"license": "MIT",
|
| 208 |
+
"optional": true,
|
| 209 |
+
"os": [
|
| 210 |
+
"linux"
|
| 211 |
+
],
|
| 212 |
+
"engines": {
|
| 213 |
+
"node": ">=18"
|
| 214 |
+
}
|
| 215 |
+
},
|
| 216 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 217 |
+
"version": "0.27.3",
|
| 218 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
| 219 |
+
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
| 220 |
+
"cpu": [
|
| 221 |
+
"ia32"
|
| 222 |
+
],
|
| 223 |
+
"dev": true,
|
| 224 |
+
"license": "MIT",
|
| 225 |
+
"optional": true,
|
| 226 |
+
"os": [
|
| 227 |
+
"linux"
|
| 228 |
+
],
|
| 229 |
+
"engines": {
|
| 230 |
+
"node": ">=18"
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 234 |
+
"version": "0.27.3",
|
| 235 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
| 236 |
+
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
| 237 |
+
"cpu": [
|
| 238 |
+
"loong64"
|
| 239 |
+
],
|
| 240 |
+
"dev": true,
|
| 241 |
+
"license": "MIT",
|
| 242 |
+
"optional": true,
|
| 243 |
+
"os": [
|
| 244 |
+
"linux"
|
| 245 |
+
],
|
| 246 |
+
"engines": {
|
| 247 |
+
"node": ">=18"
|
| 248 |
+
}
|
| 249 |
+
},
|
| 250 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 251 |
+
"version": "0.27.3",
|
| 252 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
| 253 |
+
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
| 254 |
+
"cpu": [
|
| 255 |
+
"mips64el"
|
| 256 |
+
],
|
| 257 |
+
"dev": true,
|
| 258 |
+
"license": "MIT",
|
| 259 |
+
"optional": true,
|
| 260 |
+
"os": [
|
| 261 |
+
"linux"
|
| 262 |
+
],
|
| 263 |
+
"engines": {
|
| 264 |
+
"node": ">=18"
|
| 265 |
+
}
|
| 266 |
+
},
|
| 267 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 268 |
+
"version": "0.27.3",
|
| 269 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
| 270 |
+
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
| 271 |
+
"cpu": [
|
| 272 |
+
"ppc64"
|
| 273 |
+
],
|
| 274 |
+
"dev": true,
|
| 275 |
+
"license": "MIT",
|
| 276 |
+
"optional": true,
|
| 277 |
+
"os": [
|
| 278 |
+
"linux"
|
| 279 |
+
],
|
| 280 |
+
"engines": {
|
| 281 |
+
"node": ">=18"
|
| 282 |
+
}
|
| 283 |
+
},
|
| 284 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 285 |
+
"version": "0.27.3",
|
| 286 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
| 287 |
+
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
| 288 |
+
"cpu": [
|
| 289 |
+
"riscv64"
|
| 290 |
+
],
|
| 291 |
+
"dev": true,
|
| 292 |
+
"license": "MIT",
|
| 293 |
+
"optional": true,
|
| 294 |
+
"os": [
|
| 295 |
+
"linux"
|
| 296 |
+
],
|
| 297 |
+
"engines": {
|
| 298 |
+
"node": ">=18"
|
| 299 |
+
}
|
| 300 |
+
},
|
| 301 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 302 |
+
"version": "0.27.3",
|
| 303 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
| 304 |
+
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
| 305 |
+
"cpu": [
|
| 306 |
+
"s390x"
|
| 307 |
+
],
|
| 308 |
+
"dev": true,
|
| 309 |
+
"license": "MIT",
|
| 310 |
+
"optional": true,
|
| 311 |
+
"os": [
|
| 312 |
+
"linux"
|
| 313 |
+
],
|
| 314 |
+
"engines": {
|
| 315 |
+
"node": ">=18"
|
| 316 |
+
}
|
| 317 |
+
},
|
| 318 |
+
"node_modules/@esbuild/linux-x64": {
|
| 319 |
+
"version": "0.27.3",
|
| 320 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
| 321 |
+
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
| 322 |
+
"cpu": [
|
| 323 |
+
"x64"
|
| 324 |
+
],
|
| 325 |
+
"dev": true,
|
| 326 |
+
"license": "MIT",
|
| 327 |
+
"optional": true,
|
| 328 |
+
"os": [
|
| 329 |
+
"linux"
|
| 330 |
+
],
|
| 331 |
+
"engines": {
|
| 332 |
+
"node": ">=18"
|
| 333 |
+
}
|
| 334 |
+
},
|
| 335 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 336 |
+
"version": "0.27.3",
|
| 337 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
| 338 |
+
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
| 339 |
+
"cpu": [
|
| 340 |
+
"arm64"
|
| 341 |
+
],
|
| 342 |
+
"dev": true,
|
| 343 |
+
"license": "MIT",
|
| 344 |
+
"optional": true,
|
| 345 |
+
"os": [
|
| 346 |
+
"netbsd"
|
| 347 |
+
],
|
| 348 |
+
"engines": {
|
| 349 |
+
"node": ">=18"
|
| 350 |
+
}
|
| 351 |
+
},
|
| 352 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 353 |
+
"version": "0.27.3",
|
| 354 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
| 355 |
+
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
| 356 |
+
"cpu": [
|
| 357 |
+
"x64"
|
| 358 |
+
],
|
| 359 |
+
"dev": true,
|
| 360 |
+
"license": "MIT",
|
| 361 |
+
"optional": true,
|
| 362 |
+
"os": [
|
| 363 |
+
"netbsd"
|
| 364 |
+
],
|
| 365 |
+
"engines": {
|
| 366 |
+
"node": ">=18"
|
| 367 |
+
}
|
| 368 |
+
},
|
| 369 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 370 |
+
"version": "0.27.3",
|
| 371 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
| 372 |
+
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
| 373 |
+
"cpu": [
|
| 374 |
+
"arm64"
|
| 375 |
+
],
|
| 376 |
+
"dev": true,
|
| 377 |
+
"license": "MIT",
|
| 378 |
+
"optional": true,
|
| 379 |
+
"os": [
|
| 380 |
+
"openbsd"
|
| 381 |
+
],
|
| 382 |
+
"engines": {
|
| 383 |
+
"node": ">=18"
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 387 |
+
"version": "0.27.3",
|
| 388 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
| 389 |
+
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
| 390 |
+
"cpu": [
|
| 391 |
+
"x64"
|
| 392 |
+
],
|
| 393 |
+
"dev": true,
|
| 394 |
+
"license": "MIT",
|
| 395 |
+
"optional": true,
|
| 396 |
+
"os": [
|
| 397 |
+
"openbsd"
|
| 398 |
+
],
|
| 399 |
+
"engines": {
|
| 400 |
+
"node": ">=18"
|
| 401 |
+
}
|
| 402 |
+
},
|
| 403 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 404 |
+
"version": "0.27.3",
|
| 405 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
| 406 |
+
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
| 407 |
+
"cpu": [
|
| 408 |
+
"arm64"
|
| 409 |
+
],
|
| 410 |
+
"dev": true,
|
| 411 |
+
"license": "MIT",
|
| 412 |
+
"optional": true,
|
| 413 |
+
"os": [
|
| 414 |
+
"openharmony"
|
| 415 |
+
],
|
| 416 |
+
"engines": {
|
| 417 |
+
"node": ">=18"
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 421 |
+
"version": "0.27.3",
|
| 422 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
| 423 |
+
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
| 424 |
+
"cpu": [
|
| 425 |
+
"x64"
|
| 426 |
+
],
|
| 427 |
+
"dev": true,
|
| 428 |
+
"license": "MIT",
|
| 429 |
+
"optional": true,
|
| 430 |
+
"os": [
|
| 431 |
+
"sunos"
|
| 432 |
+
],
|
| 433 |
+
"engines": {
|
| 434 |
+
"node": ">=18"
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 438 |
+
"version": "0.27.3",
|
| 439 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
| 440 |
+
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
| 441 |
+
"cpu": [
|
| 442 |
+
"arm64"
|
| 443 |
+
],
|
| 444 |
+
"dev": true,
|
| 445 |
+
"license": "MIT",
|
| 446 |
+
"optional": true,
|
| 447 |
+
"os": [
|
| 448 |
+
"win32"
|
| 449 |
+
],
|
| 450 |
+
"engines": {
|
| 451 |
+
"node": ">=18"
|
| 452 |
+
}
|
| 453 |
+
},
|
| 454 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 455 |
+
"version": "0.27.3",
|
| 456 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
| 457 |
+
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
| 458 |
+
"cpu": [
|
| 459 |
+
"ia32"
|
| 460 |
+
],
|
| 461 |
+
"dev": true,
|
| 462 |
+
"license": "MIT",
|
| 463 |
+
"optional": true,
|
| 464 |
+
"os": [
|
| 465 |
+
"win32"
|
| 466 |
+
],
|
| 467 |
+
"engines": {
|
| 468 |
+
"node": ">=18"
|
| 469 |
+
}
|
| 470 |
+
},
|
| 471 |
+
"node_modules/@esbuild/win32-x64": {
|
| 472 |
+
"version": "0.27.3",
|
| 473 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
| 474 |
+
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
| 475 |
+
"cpu": [
|
| 476 |
+
"x64"
|
| 477 |
+
],
|
| 478 |
+
"dev": true,
|
| 479 |
+
"license": "MIT",
|
| 480 |
+
"optional": true,
|
| 481 |
+
"os": [
|
| 482 |
+
"win32"
|
| 483 |
+
],
|
| 484 |
+
"engines": {
|
| 485 |
+
"node": ">=18"
|
| 486 |
+
}
|
| 487 |
+
},
|
| 488 |
+
"node_modules/@img/colour": {
|
| 489 |
+
"version": "1.1.0",
|
| 490 |
+
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
| 491 |
+
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
| 492 |
+
"license": "MIT",
|
| 493 |
+
"optional": true,
|
| 494 |
+
"engines": {
|
| 495 |
+
"node": ">=18"
|
| 496 |
+
}
|
| 497 |
+
},
|
| 498 |
+
"node_modules/@img/sharp-darwin-arm64": {
|
| 499 |
+
"version": "0.34.5",
|
| 500 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
| 501 |
+
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
| 502 |
+
"cpu": [
|
| 503 |
+
"arm64"
|
| 504 |
+
],
|
| 505 |
+
"license": "Apache-2.0",
|
| 506 |
+
"optional": true,
|
| 507 |
+
"os": [
|
| 508 |
+
"darwin"
|
| 509 |
+
],
|
| 510 |
+
"engines": {
|
| 511 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 512 |
+
},
|
| 513 |
+
"funding": {
|
| 514 |
+
"url": "https://opencollective.com/libvips"
|
| 515 |
+
},
|
| 516 |
+
"optionalDependencies": {
|
| 517 |
+
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
| 518 |
+
}
|
| 519 |
+
},
|
| 520 |
+
"node_modules/@img/sharp-darwin-x64": {
|
| 521 |
+
"version": "0.34.5",
|
| 522 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
| 523 |
+
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
| 524 |
+
"cpu": [
|
| 525 |
+
"x64"
|
| 526 |
+
],
|
| 527 |
+
"license": "Apache-2.0",
|
| 528 |
+
"optional": true,
|
| 529 |
+
"os": [
|
| 530 |
+
"darwin"
|
| 531 |
+
],
|
| 532 |
+
"engines": {
|
| 533 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 534 |
+
},
|
| 535 |
+
"funding": {
|
| 536 |
+
"url": "https://opencollective.com/libvips"
|
| 537 |
+
},
|
| 538 |
+
"optionalDependencies": {
|
| 539 |
+
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
| 540 |
+
}
|
| 541 |
+
},
|
| 542 |
+
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
| 543 |
+
"version": "1.2.4",
|
| 544 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
| 545 |
+
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
| 546 |
+
"cpu": [
|
| 547 |
+
"arm64"
|
| 548 |
+
],
|
| 549 |
+
"license": "LGPL-3.0-or-later",
|
| 550 |
+
"optional": true,
|
| 551 |
+
"os": [
|
| 552 |
+
"darwin"
|
| 553 |
+
],
|
| 554 |
+
"funding": {
|
| 555 |
+
"url": "https://opencollective.com/libvips"
|
| 556 |
+
}
|
| 557 |
+
},
|
| 558 |
+
"node_modules/@img/sharp-libvips-darwin-x64": {
|
| 559 |
+
"version": "1.2.4",
|
| 560 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
| 561 |
+
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
| 562 |
+
"cpu": [
|
| 563 |
+
"x64"
|
| 564 |
+
],
|
| 565 |
+
"license": "LGPL-3.0-or-later",
|
| 566 |
+
"optional": true,
|
| 567 |
+
"os": [
|
| 568 |
+
"darwin"
|
| 569 |
+
],
|
| 570 |
+
"funding": {
|
| 571 |
+
"url": "https://opencollective.com/libvips"
|
| 572 |
+
}
|
| 573 |
+
},
|
| 574 |
+
"node_modules/@img/sharp-libvips-linux-arm": {
|
| 575 |
+
"version": "1.2.4",
|
| 576 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
| 577 |
+
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
| 578 |
+
"cpu": [
|
| 579 |
+
"arm"
|
| 580 |
+
],
|
| 581 |
+
"license": "LGPL-3.0-or-later",
|
| 582 |
+
"optional": true,
|
| 583 |
+
"os": [
|
| 584 |
+
"linux"
|
| 585 |
+
],
|
| 586 |
+
"funding": {
|
| 587 |
+
"url": "https://opencollective.com/libvips"
|
| 588 |
+
}
|
| 589 |
+
},
|
| 590 |
+
"node_modules/@img/sharp-libvips-linux-arm64": {
|
| 591 |
+
"version": "1.2.4",
|
| 592 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
| 593 |
+
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
| 594 |
+
"cpu": [
|
| 595 |
+
"arm64"
|
| 596 |
+
],
|
| 597 |
+
"license": "LGPL-3.0-or-later",
|
| 598 |
+
"optional": true,
|
| 599 |
+
"os": [
|
| 600 |
+
"linux"
|
| 601 |
+
],
|
| 602 |
+
"funding": {
|
| 603 |
+
"url": "https://opencollective.com/libvips"
|
| 604 |
+
}
|
| 605 |
+
},
|
| 606 |
+
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
| 607 |
+
"version": "1.2.4",
|
| 608 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
| 609 |
+
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
| 610 |
+
"cpu": [
|
| 611 |
+
"ppc64"
|
| 612 |
+
],
|
| 613 |
+
"license": "LGPL-3.0-or-later",
|
| 614 |
+
"optional": true,
|
| 615 |
+
"os": [
|
| 616 |
+
"linux"
|
| 617 |
+
],
|
| 618 |
+
"funding": {
|
| 619 |
+
"url": "https://opencollective.com/libvips"
|
| 620 |
+
}
|
| 621 |
+
},
|
| 622 |
+
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
| 623 |
+
"version": "1.2.4",
|
| 624 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
| 625 |
+
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
| 626 |
+
"cpu": [
|
| 627 |
+
"riscv64"
|
| 628 |
+
],
|
| 629 |
+
"license": "LGPL-3.0-or-later",
|
| 630 |
+
"optional": true,
|
| 631 |
+
"os": [
|
| 632 |
+
"linux"
|
| 633 |
+
],
|
| 634 |
+
"funding": {
|
| 635 |
+
"url": "https://opencollective.com/libvips"
|
| 636 |
+
}
|
| 637 |
+
},
|
| 638 |
+
"node_modules/@img/sharp-libvips-linux-s390x": {
|
| 639 |
+
"version": "1.2.4",
|
| 640 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
| 641 |
+
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
| 642 |
+
"cpu": [
|
| 643 |
+
"s390x"
|
| 644 |
+
],
|
| 645 |
+
"license": "LGPL-3.0-or-later",
|
| 646 |
+
"optional": true,
|
| 647 |
+
"os": [
|
| 648 |
+
"linux"
|
| 649 |
+
],
|
| 650 |
+
"funding": {
|
| 651 |
+
"url": "https://opencollective.com/libvips"
|
| 652 |
+
}
|
| 653 |
+
},
|
| 654 |
+
"node_modules/@img/sharp-libvips-linux-x64": {
|
| 655 |
+
"version": "1.2.4",
|
| 656 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
| 657 |
+
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
| 658 |
+
"cpu": [
|
| 659 |
+
"x64"
|
| 660 |
+
],
|
| 661 |
+
"license": "LGPL-3.0-or-later",
|
| 662 |
+
"optional": true,
|
| 663 |
+
"os": [
|
| 664 |
+
"linux"
|
| 665 |
+
],
|
| 666 |
+
"funding": {
|
| 667 |
+
"url": "https://opencollective.com/libvips"
|
| 668 |
+
}
|
| 669 |
+
},
|
| 670 |
+
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
| 671 |
+
"version": "1.2.4",
|
| 672 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
| 673 |
+
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
| 674 |
+
"cpu": [
|
| 675 |
+
"arm64"
|
| 676 |
+
],
|
| 677 |
+
"license": "LGPL-3.0-or-later",
|
| 678 |
+
"optional": true,
|
| 679 |
+
"os": [
|
| 680 |
+
"linux"
|
| 681 |
+
],
|
| 682 |
+
"funding": {
|
| 683 |
+
"url": "https://opencollective.com/libvips"
|
| 684 |
+
}
|
| 685 |
+
},
|
| 686 |
+
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
| 687 |
+
"version": "1.2.4",
|
| 688 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
| 689 |
+
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
| 690 |
+
"cpu": [
|
| 691 |
+
"x64"
|
| 692 |
+
],
|
| 693 |
+
"license": "LGPL-3.0-or-later",
|
| 694 |
+
"optional": true,
|
| 695 |
+
"os": [
|
| 696 |
+
"linux"
|
| 697 |
+
],
|
| 698 |
+
"funding": {
|
| 699 |
+
"url": "https://opencollective.com/libvips"
|
| 700 |
+
}
|
| 701 |
+
},
|
| 702 |
+
"node_modules/@img/sharp-linux-arm": {
|
| 703 |
+
"version": "0.34.5",
|
| 704 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
| 705 |
+
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
| 706 |
+
"cpu": [
|
| 707 |
+
"arm"
|
| 708 |
+
],
|
| 709 |
+
"license": "Apache-2.0",
|
| 710 |
+
"optional": true,
|
| 711 |
+
"os": [
|
| 712 |
+
"linux"
|
| 713 |
+
],
|
| 714 |
+
"engines": {
|
| 715 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 716 |
+
},
|
| 717 |
+
"funding": {
|
| 718 |
+
"url": "https://opencollective.com/libvips"
|
| 719 |
+
},
|
| 720 |
+
"optionalDependencies": {
|
| 721 |
+
"@img/sharp-libvips-linux-arm": "1.2.4"
|
| 722 |
+
}
|
| 723 |
+
},
|
| 724 |
+
"node_modules/@img/sharp-linux-arm64": {
|
| 725 |
+
"version": "0.34.5",
|
| 726 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
| 727 |
+
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
| 728 |
+
"cpu": [
|
| 729 |
+
"arm64"
|
| 730 |
+
],
|
| 731 |
+
"license": "Apache-2.0",
|
| 732 |
+
"optional": true,
|
| 733 |
+
"os": [
|
| 734 |
+
"linux"
|
| 735 |
+
],
|
| 736 |
+
"engines": {
|
| 737 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 738 |
+
},
|
| 739 |
+
"funding": {
|
| 740 |
+
"url": "https://opencollective.com/libvips"
|
| 741 |
+
},
|
| 742 |
+
"optionalDependencies": {
|
| 743 |
+
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
| 744 |
+
}
|
| 745 |
+
},
|
| 746 |
+
"node_modules/@img/sharp-linux-ppc64": {
|
| 747 |
+
"version": "0.34.5",
|
| 748 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
| 749 |
+
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
| 750 |
+
"cpu": [
|
| 751 |
+
"ppc64"
|
| 752 |
+
],
|
| 753 |
+
"license": "Apache-2.0",
|
| 754 |
+
"optional": true,
|
| 755 |
+
"os": [
|
| 756 |
+
"linux"
|
| 757 |
+
],
|
| 758 |
+
"engines": {
|
| 759 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 760 |
+
},
|
| 761 |
+
"funding": {
|
| 762 |
+
"url": "https://opencollective.com/libvips"
|
| 763 |
+
},
|
| 764 |
+
"optionalDependencies": {
|
| 765 |
+
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
| 766 |
+
}
|
| 767 |
+
},
|
| 768 |
+
"node_modules/@img/sharp-linux-riscv64": {
|
| 769 |
+
"version": "0.34.5",
|
| 770 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
| 771 |
+
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
| 772 |
+
"cpu": [
|
| 773 |
+
"riscv64"
|
| 774 |
+
],
|
| 775 |
+
"license": "Apache-2.0",
|
| 776 |
+
"optional": true,
|
| 777 |
+
"os": [
|
| 778 |
+
"linux"
|
| 779 |
+
],
|
| 780 |
+
"engines": {
|
| 781 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 782 |
+
},
|
| 783 |
+
"funding": {
|
| 784 |
+
"url": "https://opencollective.com/libvips"
|
| 785 |
+
},
|
| 786 |
+
"optionalDependencies": {
|
| 787 |
+
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
| 788 |
+
}
|
| 789 |
+
},
|
| 790 |
+
"node_modules/@img/sharp-linux-s390x": {
|
| 791 |
+
"version": "0.34.5",
|
| 792 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
| 793 |
+
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
| 794 |
+
"cpu": [
|
| 795 |
+
"s390x"
|
| 796 |
+
],
|
| 797 |
+
"license": "Apache-2.0",
|
| 798 |
+
"optional": true,
|
| 799 |
+
"os": [
|
| 800 |
+
"linux"
|
| 801 |
+
],
|
| 802 |
+
"engines": {
|
| 803 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 804 |
+
},
|
| 805 |
+
"funding": {
|
| 806 |
+
"url": "https://opencollective.com/libvips"
|
| 807 |
+
},
|
| 808 |
+
"optionalDependencies": {
|
| 809 |
+
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
| 810 |
+
}
|
| 811 |
+
},
|
| 812 |
+
"node_modules/@img/sharp-linux-x64": {
|
| 813 |
+
"version": "0.34.5",
|
| 814 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
| 815 |
+
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
| 816 |
+
"cpu": [
|
| 817 |
+
"x64"
|
| 818 |
+
],
|
| 819 |
+
"license": "Apache-2.0",
|
| 820 |
+
"optional": true,
|
| 821 |
+
"os": [
|
| 822 |
+
"linux"
|
| 823 |
+
],
|
| 824 |
+
"engines": {
|
| 825 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 826 |
+
},
|
| 827 |
+
"funding": {
|
| 828 |
+
"url": "https://opencollective.com/libvips"
|
| 829 |
+
},
|
| 830 |
+
"optionalDependencies": {
|
| 831 |
+
"@img/sharp-libvips-linux-x64": "1.2.4"
|
| 832 |
+
}
|
| 833 |
+
},
|
| 834 |
+
"node_modules/@img/sharp-linuxmusl-arm64": {
|
| 835 |
+
"version": "0.34.5",
|
| 836 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
| 837 |
+
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
| 838 |
+
"cpu": [
|
| 839 |
+
"arm64"
|
| 840 |
+
],
|
| 841 |
+
"license": "Apache-2.0",
|
| 842 |
+
"optional": true,
|
| 843 |
+
"os": [
|
| 844 |
+
"linux"
|
| 845 |
+
],
|
| 846 |
+
"engines": {
|
| 847 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 848 |
+
},
|
| 849 |
+
"funding": {
|
| 850 |
+
"url": "https://opencollective.com/libvips"
|
| 851 |
+
},
|
| 852 |
+
"optionalDependencies": {
|
| 853 |
+
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
| 854 |
+
}
|
| 855 |
+
},
|
| 856 |
+
"node_modules/@img/sharp-linuxmusl-x64": {
|
| 857 |
+
"version": "0.34.5",
|
| 858 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
| 859 |
+
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
| 860 |
+
"cpu": [
|
| 861 |
+
"x64"
|
| 862 |
+
],
|
| 863 |
+
"license": "Apache-2.0",
|
| 864 |
+
"optional": true,
|
| 865 |
+
"os": [
|
| 866 |
+
"linux"
|
| 867 |
+
],
|
| 868 |
+
"engines": {
|
| 869 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 870 |
+
},
|
| 871 |
+
"funding": {
|
| 872 |
+
"url": "https://opencollective.com/libvips"
|
| 873 |
+
},
|
| 874 |
+
"optionalDependencies": {
|
| 875 |
+
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
| 876 |
+
}
|
| 877 |
+
},
|
| 878 |
+
"node_modules/@img/sharp-wasm32": {
|
| 879 |
+
"version": "0.34.5",
|
| 880 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
| 881 |
+
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
| 882 |
+
"cpu": [
|
| 883 |
+
"wasm32"
|
| 884 |
+
],
|
| 885 |
+
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
| 886 |
+
"optional": true,
|
| 887 |
+
"dependencies": {
|
| 888 |
+
"@emnapi/runtime": "^1.7.0"
|
| 889 |
+
},
|
| 890 |
+
"engines": {
|
| 891 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 892 |
+
},
|
| 893 |
+
"funding": {
|
| 894 |
+
"url": "https://opencollective.com/libvips"
|
| 895 |
+
}
|
| 896 |
+
},
|
| 897 |
+
"node_modules/@img/sharp-win32-arm64": {
|
| 898 |
+
"version": "0.34.5",
|
| 899 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
| 900 |
+
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
| 901 |
+
"cpu": [
|
| 902 |
+
"arm64"
|
| 903 |
+
],
|
| 904 |
+
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
| 905 |
+
"optional": true,
|
| 906 |
+
"os": [
|
| 907 |
+
"win32"
|
| 908 |
+
],
|
| 909 |
+
"engines": {
|
| 910 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 911 |
+
},
|
| 912 |
+
"funding": {
|
| 913 |
+
"url": "https://opencollective.com/libvips"
|
| 914 |
+
}
|
| 915 |
+
},
|
| 916 |
+
"node_modules/@img/sharp-win32-ia32": {
|
| 917 |
+
"version": "0.34.5",
|
| 918 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
| 919 |
+
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
| 920 |
+
"cpu": [
|
| 921 |
+
"ia32"
|
| 922 |
+
],
|
| 923 |
+
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
| 924 |
+
"optional": true,
|
| 925 |
+
"os": [
|
| 926 |
+
"win32"
|
| 927 |
+
],
|
| 928 |
+
"engines": {
|
| 929 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 930 |
+
},
|
| 931 |
+
"funding": {
|
| 932 |
+
"url": "https://opencollective.com/libvips"
|
| 933 |
+
}
|
| 934 |
+
},
|
| 935 |
+
"node_modules/@img/sharp-win32-x64": {
|
| 936 |
+
"version": "0.34.5",
|
| 937 |
+
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
| 938 |
+
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
| 939 |
+
"cpu": [
|
| 940 |
+
"x64"
|
| 941 |
+
],
|
| 942 |
+
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
| 943 |
+
"optional": true,
|
| 944 |
+
"os": [
|
| 945 |
+
"win32"
|
| 946 |
+
],
|
| 947 |
+
"engines": {
|
| 948 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 949 |
+
},
|
| 950 |
+
"funding": {
|
| 951 |
+
"url": "https://opencollective.com/libvips"
|
| 952 |
+
}
|
| 953 |
+
},
|
| 954 |
+
"node_modules/@next/env": {
|
| 955 |
+
"version": "15.5.12",
|
| 956 |
+
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz",
|
| 957 |
+
"integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==",
|
| 958 |
+
"license": "MIT"
|
| 959 |
+
},
|
| 960 |
+
"node_modules/@next/swc-darwin-arm64": {
|
| 961 |
+
"version": "15.5.12",
|
| 962 |
+
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz",
|
| 963 |
+
"integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==",
|
| 964 |
+
"cpu": [
|
| 965 |
+
"arm64"
|
| 966 |
+
],
|
| 967 |
+
"license": "MIT",
|
| 968 |
+
"optional": true,
|
| 969 |
+
"os": [
|
| 970 |
+
"darwin"
|
| 971 |
+
],
|
| 972 |
+
"engines": {
|
| 973 |
+
"node": ">= 10"
|
| 974 |
+
}
|
| 975 |
+
},
|
| 976 |
+
"node_modules/@next/swc-darwin-x64": {
|
| 977 |
+
"version": "15.5.12",
|
| 978 |
+
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz",
|
| 979 |
+
"integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==",
|
| 980 |
+
"cpu": [
|
| 981 |
+
"x64"
|
| 982 |
+
],
|
| 983 |
+
"license": "MIT",
|
| 984 |
+
"optional": true,
|
| 985 |
+
"os": [
|
| 986 |
+
"darwin"
|
| 987 |
+
],
|
| 988 |
+
"engines": {
|
| 989 |
+
"node": ">= 10"
|
| 990 |
+
}
|
| 991 |
+
},
|
| 992 |
+
"node_modules/@next/swc-linux-arm64-gnu": {
|
| 993 |
+
"version": "15.5.12",
|
| 994 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz",
|
| 995 |
+
"integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==",
|
| 996 |
+
"cpu": [
|
| 997 |
+
"arm64"
|
| 998 |
+
],
|
| 999 |
+
"license": "MIT",
|
| 1000 |
+
"optional": true,
|
| 1001 |
+
"os": [
|
| 1002 |
+
"linux"
|
| 1003 |
+
],
|
| 1004 |
+
"engines": {
|
| 1005 |
+
"node": ">= 10"
|
| 1006 |
+
}
|
| 1007 |
+
},
|
| 1008 |
+
"node_modules/@next/swc-linux-arm64-musl": {
|
| 1009 |
+
"version": "15.5.12",
|
| 1010 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz",
|
| 1011 |
+
"integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==",
|
| 1012 |
+
"cpu": [
|
| 1013 |
+
"arm64"
|
| 1014 |
+
],
|
| 1015 |
+
"license": "MIT",
|
| 1016 |
+
"optional": true,
|
| 1017 |
+
"os": [
|
| 1018 |
+
"linux"
|
| 1019 |
+
],
|
| 1020 |
+
"engines": {
|
| 1021 |
+
"node": ">= 10"
|
| 1022 |
+
}
|
| 1023 |
+
},
|
| 1024 |
+
"node_modules/@next/swc-linux-x64-gnu": {
|
| 1025 |
+
"version": "15.5.12",
|
| 1026 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz",
|
| 1027 |
+
"integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==",
|
| 1028 |
+
"cpu": [
|
| 1029 |
+
"x64"
|
| 1030 |
+
],
|
| 1031 |
+
"license": "MIT",
|
| 1032 |
+
"optional": true,
|
| 1033 |
+
"os": [
|
| 1034 |
+
"linux"
|
| 1035 |
+
],
|
| 1036 |
+
"engines": {
|
| 1037 |
+
"node": ">= 10"
|
| 1038 |
+
}
|
| 1039 |
+
},
|
| 1040 |
+
"node_modules/@next/swc-linux-x64-musl": {
|
| 1041 |
+
"version": "15.5.12",
|
| 1042 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz",
|
| 1043 |
+
"integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==",
|
| 1044 |
+
"cpu": [
|
| 1045 |
+
"x64"
|
| 1046 |
+
],
|
| 1047 |
+
"license": "MIT",
|
| 1048 |
+
"optional": true,
|
| 1049 |
+
"os": [
|
| 1050 |
+
"linux"
|
| 1051 |
+
],
|
| 1052 |
+
"engines": {
|
| 1053 |
+
"node": ">= 10"
|
| 1054 |
+
}
|
| 1055 |
+
},
|
| 1056 |
+
"node_modules/@next/swc-win32-arm64-msvc": {
|
| 1057 |
+
"version": "15.5.12",
|
| 1058 |
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz",
|
| 1059 |
+
"integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==",
|
| 1060 |
+
"cpu": [
|
| 1061 |
+
"arm64"
|
| 1062 |
+
],
|
| 1063 |
+
"license": "MIT",
|
| 1064 |
+
"optional": true,
|
| 1065 |
+
"os": [
|
| 1066 |
+
"win32"
|
| 1067 |
+
],
|
| 1068 |
+
"engines": {
|
| 1069 |
+
"node": ">= 10"
|
| 1070 |
+
}
|
| 1071 |
+
},
|
| 1072 |
+
"node_modules/@next/swc-win32-x64-msvc": {
|
| 1073 |
+
"version": "15.5.12",
|
| 1074 |
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz",
|
| 1075 |
+
"integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==",
|
| 1076 |
+
"cpu": [
|
| 1077 |
+
"x64"
|
| 1078 |
+
],
|
| 1079 |
+
"license": "MIT",
|
| 1080 |
+
"optional": true,
|
| 1081 |
+
"os": [
|
| 1082 |
+
"win32"
|
| 1083 |
+
],
|
| 1084 |
+
"engines": {
|
| 1085 |
+
"node": ">= 10"
|
| 1086 |
+
}
|
| 1087 |
+
},
|
| 1088 |
+
"node_modules/@prisma/client": {
|
| 1089 |
+
"version": "6.19.2",
|
| 1090 |
+
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz",
|
| 1091 |
+
"integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==",
|
| 1092 |
+
"hasInstallScript": true,
|
| 1093 |
+
"license": "Apache-2.0",
|
| 1094 |
+
"engines": {
|
| 1095 |
+
"node": ">=18.18"
|
| 1096 |
+
},
|
| 1097 |
+
"peerDependencies": {
|
| 1098 |
+
"prisma": "*",
|
| 1099 |
+
"typescript": ">=5.1.0"
|
| 1100 |
+
},
|
| 1101 |
+
"peerDependenciesMeta": {
|
| 1102 |
+
"prisma": {
|
| 1103 |
+
"optional": true
|
| 1104 |
+
},
|
| 1105 |
+
"typescript": {
|
| 1106 |
+
"optional": true
|
| 1107 |
+
}
|
| 1108 |
+
}
|
| 1109 |
+
},
|
| 1110 |
+
"node_modules/@prisma/config": {
|
| 1111 |
+
"version": "6.19.2",
|
| 1112 |
+
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz",
|
| 1113 |
+
"integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==",
|
| 1114 |
+
"devOptional": true,
|
| 1115 |
+
"license": "Apache-2.0",
|
| 1116 |
+
"dependencies": {
|
| 1117 |
+
"c12": "3.1.0",
|
| 1118 |
+
"deepmerge-ts": "7.1.5",
|
| 1119 |
+
"effect": "3.18.4",
|
| 1120 |
+
"empathic": "2.0.0"
|
| 1121 |
+
}
|
| 1122 |
+
},
|
| 1123 |
+
"node_modules/@prisma/debug": {
|
| 1124 |
+
"version": "6.19.2",
|
| 1125 |
+
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz",
|
| 1126 |
+
"integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==",
|
| 1127 |
+
"devOptional": true,
|
| 1128 |
+
"license": "Apache-2.0"
|
| 1129 |
+
},
|
| 1130 |
+
"node_modules/@prisma/engines": {
|
| 1131 |
+
"version": "6.19.2",
|
| 1132 |
+
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz",
|
| 1133 |
+
"integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==",
|
| 1134 |
+
"devOptional": true,
|
| 1135 |
+
"hasInstallScript": true,
|
| 1136 |
+
"license": "Apache-2.0",
|
| 1137 |
+
"dependencies": {
|
| 1138 |
+
"@prisma/debug": "6.19.2",
|
| 1139 |
+
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
| 1140 |
+
"@prisma/fetch-engine": "6.19.2",
|
| 1141 |
+
"@prisma/get-platform": "6.19.2"
|
| 1142 |
+
}
|
| 1143 |
+
},
|
| 1144 |
+
"node_modules/@prisma/engines-version": {
|
| 1145 |
+
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
| 1146 |
+
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
| 1147 |
+
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
| 1148 |
+
"devOptional": true,
|
| 1149 |
+
"license": "Apache-2.0"
|
| 1150 |
+
},
|
| 1151 |
+
"node_modules/@prisma/fetch-engine": {
|
| 1152 |
+
"version": "6.19.2",
|
| 1153 |
+
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz",
|
| 1154 |
+
"integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==",
|
| 1155 |
+
"devOptional": true,
|
| 1156 |
+
"license": "Apache-2.0",
|
| 1157 |
+
"dependencies": {
|
| 1158 |
+
"@prisma/debug": "6.19.2",
|
| 1159 |
+
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
| 1160 |
+
"@prisma/get-platform": "6.19.2"
|
| 1161 |
+
}
|
| 1162 |
+
},
|
| 1163 |
+
"node_modules/@prisma/get-platform": {
|
| 1164 |
+
"version": "6.19.2",
|
| 1165 |
+
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz",
|
| 1166 |
+
"integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==",
|
| 1167 |
+
"devOptional": true,
|
| 1168 |
+
"license": "Apache-2.0",
|
| 1169 |
+
"dependencies": {
|
| 1170 |
+
"@prisma/debug": "6.19.2"
|
| 1171 |
+
}
|
| 1172 |
+
},
|
| 1173 |
+
"node_modules/@standard-schema/spec": {
|
| 1174 |
+
"version": "1.1.0",
|
| 1175 |
+
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
| 1176 |
+
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
| 1177 |
+
"devOptional": true,
|
| 1178 |
+
"license": "MIT"
|
| 1179 |
+
},
|
| 1180 |
+
"node_modules/@swc/helpers": {
|
| 1181 |
+
"version": "0.5.15",
|
| 1182 |
+
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
| 1183 |
+
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
| 1184 |
+
"license": "Apache-2.0",
|
| 1185 |
+
"dependencies": {
|
| 1186 |
+
"tslib": "^2.8.0"
|
| 1187 |
+
}
|
| 1188 |
+
},
|
| 1189 |
+
"node_modules/@types/node": {
|
| 1190 |
+
"version": "22.19.15",
|
| 1191 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
| 1192 |
+
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
| 1193 |
+
"dev": true,
|
| 1194 |
+
"license": "MIT",
|
| 1195 |
+
"dependencies": {
|
| 1196 |
+
"undici-types": "~6.21.0"
|
| 1197 |
+
}
|
| 1198 |
+
},
|
| 1199 |
+
"node_modules/@types/react": {
|
| 1200 |
+
"version": "19.2.14",
|
| 1201 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
| 1202 |
+
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
| 1203 |
+
"dev": true,
|
| 1204 |
+
"license": "MIT",
|
| 1205 |
+
"dependencies": {
|
| 1206 |
+
"csstype": "^3.2.2"
|
| 1207 |
+
}
|
| 1208 |
+
},
|
| 1209 |
+
"node_modules/@types/react-dom": {
|
| 1210 |
+
"version": "19.2.3",
|
| 1211 |
+
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
| 1212 |
+
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
| 1213 |
+
"dev": true,
|
| 1214 |
+
"license": "MIT",
|
| 1215 |
+
"peerDependencies": {
|
| 1216 |
+
"@types/react": "^19.2.0"
|
| 1217 |
+
}
|
| 1218 |
+
},
|
| 1219 |
+
"node_modules/ansi-regex": {
|
| 1220 |
+
"version": "5.0.1",
|
| 1221 |
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
| 1222 |
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
| 1223 |
+
"dev": true,
|
| 1224 |
+
"license": "MIT",
|
| 1225 |
+
"engines": {
|
| 1226 |
+
"node": ">=8"
|
| 1227 |
+
}
|
| 1228 |
+
},
|
| 1229 |
+
"node_modules/ansi-styles": {
|
| 1230 |
+
"version": "4.3.0",
|
| 1231 |
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
| 1232 |
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
| 1233 |
+
"dev": true,
|
| 1234 |
+
"license": "MIT",
|
| 1235 |
+
"dependencies": {
|
| 1236 |
+
"color-convert": "^2.0.1"
|
| 1237 |
+
},
|
| 1238 |
+
"engines": {
|
| 1239 |
+
"node": ">=8"
|
| 1240 |
+
},
|
| 1241 |
+
"funding": {
|
| 1242 |
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
| 1243 |
+
}
|
| 1244 |
+
},
|
| 1245 |
+
"node_modules/c12": {
|
| 1246 |
+
"version": "3.1.0",
|
| 1247 |
+
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
| 1248 |
+
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
| 1249 |
+
"devOptional": true,
|
| 1250 |
+
"license": "MIT",
|
| 1251 |
+
"dependencies": {
|
| 1252 |
+
"chokidar": "^4.0.3",
|
| 1253 |
+
"confbox": "^0.2.2",
|
| 1254 |
+
"defu": "^6.1.4",
|
| 1255 |
+
"dotenv": "^16.6.1",
|
| 1256 |
+
"exsolve": "^1.0.7",
|
| 1257 |
+
"giget": "^2.0.0",
|
| 1258 |
+
"jiti": "^2.4.2",
|
| 1259 |
+
"ohash": "^2.0.11",
|
| 1260 |
+
"pathe": "^2.0.3",
|
| 1261 |
+
"perfect-debounce": "^1.0.0",
|
| 1262 |
+
"pkg-types": "^2.2.0",
|
| 1263 |
+
"rc9": "^2.1.2"
|
| 1264 |
+
},
|
| 1265 |
+
"peerDependencies": {
|
| 1266 |
+
"magicast": "^0.3.5"
|
| 1267 |
+
},
|
| 1268 |
+
"peerDependenciesMeta": {
|
| 1269 |
+
"magicast": {
|
| 1270 |
+
"optional": true
|
| 1271 |
+
}
|
| 1272 |
+
}
|
| 1273 |
+
},
|
| 1274 |
+
"node_modules/caniuse-lite": {
|
| 1275 |
+
"version": "1.0.30001777",
|
| 1276 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
|
| 1277 |
+
"integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
|
| 1278 |
+
"funding": [
|
| 1279 |
+
{
|
| 1280 |
+
"type": "opencollective",
|
| 1281 |
+
"url": "https://opencollective.com/browserslist"
|
| 1282 |
+
},
|
| 1283 |
+
{
|
| 1284 |
+
"type": "tidelift",
|
| 1285 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1286 |
+
},
|
| 1287 |
+
{
|
| 1288 |
+
"type": "github",
|
| 1289 |
+
"url": "https://github.com/sponsors/ai"
|
| 1290 |
+
}
|
| 1291 |
+
],
|
| 1292 |
+
"license": "CC-BY-4.0"
|
| 1293 |
+
},
|
| 1294 |
+
"node_modules/chalk": {
|
| 1295 |
+
"version": "4.1.2",
|
| 1296 |
+
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
| 1297 |
+
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
| 1298 |
+
"dev": true,
|
| 1299 |
+
"license": "MIT",
|
| 1300 |
+
"dependencies": {
|
| 1301 |
+
"ansi-styles": "^4.1.0",
|
| 1302 |
+
"supports-color": "^7.1.0"
|
| 1303 |
+
},
|
| 1304 |
+
"engines": {
|
| 1305 |
+
"node": ">=10"
|
| 1306 |
+
},
|
| 1307 |
+
"funding": {
|
| 1308 |
+
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 1309 |
+
}
|
| 1310 |
+
},
|
| 1311 |
+
"node_modules/chalk/node_modules/supports-color": {
|
| 1312 |
+
"version": "7.2.0",
|
| 1313 |
+
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
| 1314 |
+
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
| 1315 |
+
"dev": true,
|
| 1316 |
+
"license": "MIT",
|
| 1317 |
+
"dependencies": {
|
| 1318 |
+
"has-flag": "^4.0.0"
|
| 1319 |
+
},
|
| 1320 |
+
"engines": {
|
| 1321 |
+
"node": ">=8"
|
| 1322 |
+
}
|
| 1323 |
+
},
|
| 1324 |
+
"node_modules/chokidar": {
|
| 1325 |
+
"version": "4.0.3",
|
| 1326 |
+
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
| 1327 |
+
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
| 1328 |
+
"devOptional": true,
|
| 1329 |
+
"license": "MIT",
|
| 1330 |
+
"dependencies": {
|
| 1331 |
+
"readdirp": "^4.0.1"
|
| 1332 |
+
},
|
| 1333 |
+
"engines": {
|
| 1334 |
+
"node": ">= 14.16.0"
|
| 1335 |
+
},
|
| 1336 |
+
"funding": {
|
| 1337 |
+
"url": "https://paulmillr.com/funding/"
|
| 1338 |
+
}
|
| 1339 |
+
},
|
| 1340 |
+
"node_modules/citty": {
|
| 1341 |
+
"version": "0.1.6",
|
| 1342 |
+
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
| 1343 |
+
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
| 1344 |
+
"devOptional": true,
|
| 1345 |
+
"license": "MIT",
|
| 1346 |
+
"dependencies": {
|
| 1347 |
+
"consola": "^3.2.3"
|
| 1348 |
+
}
|
| 1349 |
+
},
|
| 1350 |
+
"node_modules/client-only": {
|
| 1351 |
+
"version": "0.0.1",
|
| 1352 |
+
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
| 1353 |
+
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
| 1354 |
+
"license": "MIT"
|
| 1355 |
+
},
|
| 1356 |
+
"node_modules/cliui": {
|
| 1357 |
+
"version": "8.0.1",
|
| 1358 |
+
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
| 1359 |
+
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
| 1360 |
+
"dev": true,
|
| 1361 |
+
"license": "ISC",
|
| 1362 |
+
"dependencies": {
|
| 1363 |
+
"string-width": "^4.2.0",
|
| 1364 |
+
"strip-ansi": "^6.0.1",
|
| 1365 |
+
"wrap-ansi": "^7.0.0"
|
| 1366 |
+
},
|
| 1367 |
+
"engines": {
|
| 1368 |
+
"node": ">=12"
|
| 1369 |
+
}
|
| 1370 |
+
},
|
| 1371 |
+
"node_modules/color-convert": {
|
| 1372 |
+
"version": "2.0.1",
|
| 1373 |
+
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
| 1374 |
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
| 1375 |
+
"dev": true,
|
| 1376 |
+
"license": "MIT",
|
| 1377 |
+
"dependencies": {
|
| 1378 |
+
"color-name": "~1.1.4"
|
| 1379 |
+
},
|
| 1380 |
+
"engines": {
|
| 1381 |
+
"node": ">=7.0.0"
|
| 1382 |
+
}
|
| 1383 |
+
},
|
| 1384 |
+
"node_modules/color-name": {
|
| 1385 |
+
"version": "1.1.4",
|
| 1386 |
+
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
| 1387 |
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
| 1388 |
+
"dev": true,
|
| 1389 |
+
"license": "MIT"
|
| 1390 |
+
},
|
| 1391 |
+
"node_modules/concurrently": {
|
| 1392 |
+
"version": "9.2.1",
|
| 1393 |
+
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
| 1394 |
+
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
| 1395 |
+
"dev": true,
|
| 1396 |
+
"license": "MIT",
|
| 1397 |
+
"dependencies": {
|
| 1398 |
+
"chalk": "4.1.2",
|
| 1399 |
+
"rxjs": "7.8.2",
|
| 1400 |
+
"shell-quote": "1.8.3",
|
| 1401 |
+
"supports-color": "8.1.1",
|
| 1402 |
+
"tree-kill": "1.2.2",
|
| 1403 |
+
"yargs": "17.7.2"
|
| 1404 |
+
},
|
| 1405 |
+
"bin": {
|
| 1406 |
+
"conc": "dist/bin/concurrently.js",
|
| 1407 |
+
"concurrently": "dist/bin/concurrently.js"
|
| 1408 |
+
},
|
| 1409 |
+
"engines": {
|
| 1410 |
+
"node": ">=18"
|
| 1411 |
+
},
|
| 1412 |
+
"funding": {
|
| 1413 |
+
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
| 1414 |
+
}
|
| 1415 |
+
},
|
| 1416 |
+
"node_modules/confbox": {
|
| 1417 |
+
"version": "0.2.4",
|
| 1418 |
+
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
| 1419 |
+
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
| 1420 |
+
"devOptional": true,
|
| 1421 |
+
"license": "MIT"
|
| 1422 |
+
},
|
| 1423 |
+
"node_modules/consola": {
|
| 1424 |
+
"version": "3.4.2",
|
| 1425 |
+
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
| 1426 |
+
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
| 1427 |
+
"devOptional": true,
|
| 1428 |
+
"license": "MIT",
|
| 1429 |
+
"engines": {
|
| 1430 |
+
"node": "^14.18.0 || >=16.10.0"
|
| 1431 |
+
}
|
| 1432 |
+
},
|
| 1433 |
+
"node_modules/csstype": {
|
| 1434 |
+
"version": "3.2.3",
|
| 1435 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 1436 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 1437 |
+
"dev": true,
|
| 1438 |
+
"license": "MIT"
|
| 1439 |
+
},
|
| 1440 |
+
"node_modules/deepmerge-ts": {
|
| 1441 |
+
"version": "7.1.5",
|
| 1442 |
+
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
| 1443 |
+
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
| 1444 |
+
"devOptional": true,
|
| 1445 |
+
"license": "BSD-3-Clause",
|
| 1446 |
+
"engines": {
|
| 1447 |
+
"node": ">=16.0.0"
|
| 1448 |
+
}
|
| 1449 |
+
},
|
| 1450 |
+
"node_modules/defu": {
|
| 1451 |
+
"version": "6.1.4",
|
| 1452 |
+
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
| 1453 |
+
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
| 1454 |
+
"devOptional": true,
|
| 1455 |
+
"license": "MIT"
|
| 1456 |
+
},
|
| 1457 |
+
"node_modules/destr": {
|
| 1458 |
+
"version": "2.0.5",
|
| 1459 |
+
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
| 1460 |
+
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
| 1461 |
+
"devOptional": true,
|
| 1462 |
+
"license": "MIT"
|
| 1463 |
+
},
|
| 1464 |
+
"node_modules/detect-libc": {
|
| 1465 |
+
"version": "2.1.2",
|
| 1466 |
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
| 1467 |
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
| 1468 |
+
"license": "Apache-2.0",
|
| 1469 |
+
"optional": true,
|
| 1470 |
+
"engines": {
|
| 1471 |
+
"node": ">=8"
|
| 1472 |
+
}
|
| 1473 |
+
},
|
| 1474 |
+
"node_modules/dotenv": {
|
| 1475 |
+
"version": "16.6.1",
|
| 1476 |
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
| 1477 |
+
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
| 1478 |
+
"devOptional": true,
|
| 1479 |
+
"license": "BSD-2-Clause",
|
| 1480 |
+
"engines": {
|
| 1481 |
+
"node": ">=12"
|
| 1482 |
+
},
|
| 1483 |
+
"funding": {
|
| 1484 |
+
"url": "https://dotenvx.com"
|
| 1485 |
+
}
|
| 1486 |
+
},
|
| 1487 |
+
"node_modules/effect": {
|
| 1488 |
+
"version": "3.18.4",
|
| 1489 |
+
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
|
| 1490 |
+
"integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
|
| 1491 |
+
"devOptional": true,
|
| 1492 |
+
"license": "MIT",
|
| 1493 |
+
"dependencies": {
|
| 1494 |
+
"@standard-schema/spec": "^1.0.0",
|
| 1495 |
+
"fast-check": "^3.23.1"
|
| 1496 |
+
}
|
| 1497 |
+
},
|
| 1498 |
+
"node_modules/emoji-regex": {
|
| 1499 |
+
"version": "8.0.0",
|
| 1500 |
+
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
| 1501 |
+
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
| 1502 |
+
"dev": true,
|
| 1503 |
+
"license": "MIT"
|
| 1504 |
+
},
|
| 1505 |
+
"node_modules/empathic": {
|
| 1506 |
+
"version": "2.0.0",
|
| 1507 |
+
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
| 1508 |
+
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
| 1509 |
+
"devOptional": true,
|
| 1510 |
+
"license": "MIT",
|
| 1511 |
+
"engines": {
|
| 1512 |
+
"node": ">=14"
|
| 1513 |
+
}
|
| 1514 |
+
},
|
| 1515 |
+
"node_modules/esbuild": {
|
| 1516 |
+
"version": "0.27.3",
|
| 1517 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
| 1518 |
+
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
| 1519 |
+
"dev": true,
|
| 1520 |
+
"hasInstallScript": true,
|
| 1521 |
+
"license": "MIT",
|
| 1522 |
+
"bin": {
|
| 1523 |
+
"esbuild": "bin/esbuild"
|
| 1524 |
+
},
|
| 1525 |
+
"engines": {
|
| 1526 |
+
"node": ">=18"
|
| 1527 |
+
},
|
| 1528 |
+
"optionalDependencies": {
|
| 1529 |
+
"@esbuild/aix-ppc64": "0.27.3",
|
| 1530 |
+
"@esbuild/android-arm": "0.27.3",
|
| 1531 |
+
"@esbuild/android-arm64": "0.27.3",
|
| 1532 |
+
"@esbuild/android-x64": "0.27.3",
|
| 1533 |
+
"@esbuild/darwin-arm64": "0.27.3",
|
| 1534 |
+
"@esbuild/darwin-x64": "0.27.3",
|
| 1535 |
+
"@esbuild/freebsd-arm64": "0.27.3",
|
| 1536 |
+
"@esbuild/freebsd-x64": "0.27.3",
|
| 1537 |
+
"@esbuild/linux-arm": "0.27.3",
|
| 1538 |
+
"@esbuild/linux-arm64": "0.27.3",
|
| 1539 |
+
"@esbuild/linux-ia32": "0.27.3",
|
| 1540 |
+
"@esbuild/linux-loong64": "0.27.3",
|
| 1541 |
+
"@esbuild/linux-mips64el": "0.27.3",
|
| 1542 |
+
"@esbuild/linux-ppc64": "0.27.3",
|
| 1543 |
+
"@esbuild/linux-riscv64": "0.27.3",
|
| 1544 |
+
"@esbuild/linux-s390x": "0.27.3",
|
| 1545 |
+
"@esbuild/linux-x64": "0.27.3",
|
| 1546 |
+
"@esbuild/netbsd-arm64": "0.27.3",
|
| 1547 |
+
"@esbuild/netbsd-x64": "0.27.3",
|
| 1548 |
+
"@esbuild/openbsd-arm64": "0.27.3",
|
| 1549 |
+
"@esbuild/openbsd-x64": "0.27.3",
|
| 1550 |
+
"@esbuild/openharmony-arm64": "0.27.3",
|
| 1551 |
+
"@esbuild/sunos-x64": "0.27.3",
|
| 1552 |
+
"@esbuild/win32-arm64": "0.27.3",
|
| 1553 |
+
"@esbuild/win32-ia32": "0.27.3",
|
| 1554 |
+
"@esbuild/win32-x64": "0.27.3"
|
| 1555 |
+
}
|
| 1556 |
+
},
|
| 1557 |
+
"node_modules/escalade": {
|
| 1558 |
+
"version": "3.2.0",
|
| 1559 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1560 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1561 |
+
"dev": true,
|
| 1562 |
+
"license": "MIT",
|
| 1563 |
+
"engines": {
|
| 1564 |
+
"node": ">=6"
|
| 1565 |
+
}
|
| 1566 |
+
},
|
| 1567 |
+
"node_modules/exsolve": {
|
| 1568 |
+
"version": "1.0.8",
|
| 1569 |
+
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
| 1570 |
+
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
| 1571 |
+
"devOptional": true,
|
| 1572 |
+
"license": "MIT"
|
| 1573 |
+
},
|
| 1574 |
+
"node_modules/fast-check": {
|
| 1575 |
+
"version": "3.23.2",
|
| 1576 |
+
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
| 1577 |
+
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
| 1578 |
+
"devOptional": true,
|
| 1579 |
+
"funding": [
|
| 1580 |
+
{
|
| 1581 |
+
"type": "individual",
|
| 1582 |
+
"url": "https://github.com/sponsors/dubzzz"
|
| 1583 |
+
},
|
| 1584 |
+
{
|
| 1585 |
+
"type": "opencollective",
|
| 1586 |
+
"url": "https://opencollective.com/fast-check"
|
| 1587 |
+
}
|
| 1588 |
+
],
|
| 1589 |
+
"license": "MIT",
|
| 1590 |
+
"dependencies": {
|
| 1591 |
+
"pure-rand": "^6.1.0"
|
| 1592 |
+
},
|
| 1593 |
+
"engines": {
|
| 1594 |
+
"node": ">=8.0.0"
|
| 1595 |
+
}
|
| 1596 |
+
},
|
| 1597 |
+
"node_modules/fsevents": {
|
| 1598 |
+
"version": "2.3.3",
|
| 1599 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1600 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1601 |
+
"dev": true,
|
| 1602 |
+
"hasInstallScript": true,
|
| 1603 |
+
"license": "MIT",
|
| 1604 |
+
"optional": true,
|
| 1605 |
+
"os": [
|
| 1606 |
+
"darwin"
|
| 1607 |
+
],
|
| 1608 |
+
"engines": {
|
| 1609 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1610 |
+
}
|
| 1611 |
+
},
|
| 1612 |
+
"node_modules/get-caller-file": {
|
| 1613 |
+
"version": "2.0.5",
|
| 1614 |
+
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
| 1615 |
+
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
| 1616 |
+
"dev": true,
|
| 1617 |
+
"license": "ISC",
|
| 1618 |
+
"engines": {
|
| 1619 |
+
"node": "6.* || 8.* || >= 10.*"
|
| 1620 |
+
}
|
| 1621 |
+
},
|
| 1622 |
+
"node_modules/get-tsconfig": {
|
| 1623 |
+
"version": "4.13.6",
|
| 1624 |
+
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
| 1625 |
+
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
| 1626 |
+
"dev": true,
|
| 1627 |
+
"license": "MIT",
|
| 1628 |
+
"dependencies": {
|
| 1629 |
+
"resolve-pkg-maps": "^1.0.0"
|
| 1630 |
+
},
|
| 1631 |
+
"funding": {
|
| 1632 |
+
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
| 1633 |
+
}
|
| 1634 |
+
},
|
| 1635 |
+
"node_modules/giget": {
|
| 1636 |
+
"version": "2.0.0",
|
| 1637 |
+
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
| 1638 |
+
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
| 1639 |
+
"devOptional": true,
|
| 1640 |
+
"license": "MIT",
|
| 1641 |
+
"dependencies": {
|
| 1642 |
+
"citty": "^0.1.6",
|
| 1643 |
+
"consola": "^3.4.0",
|
| 1644 |
+
"defu": "^6.1.4",
|
| 1645 |
+
"node-fetch-native": "^1.6.6",
|
| 1646 |
+
"nypm": "^0.6.0",
|
| 1647 |
+
"pathe": "^2.0.3"
|
| 1648 |
+
},
|
| 1649 |
+
"bin": {
|
| 1650 |
+
"giget": "dist/cli.mjs"
|
| 1651 |
+
}
|
| 1652 |
+
},
|
| 1653 |
+
"node_modules/has-flag": {
|
| 1654 |
+
"version": "4.0.0",
|
| 1655 |
+
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
| 1656 |
+
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
| 1657 |
+
"dev": true,
|
| 1658 |
+
"license": "MIT",
|
| 1659 |
+
"engines": {
|
| 1660 |
+
"node": ">=8"
|
| 1661 |
+
}
|
| 1662 |
+
},
|
| 1663 |
+
"node_modules/is-fullwidth-code-point": {
|
| 1664 |
+
"version": "3.0.0",
|
| 1665 |
+
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
| 1666 |
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
| 1667 |
+
"dev": true,
|
| 1668 |
+
"license": "MIT",
|
| 1669 |
+
"engines": {
|
| 1670 |
+
"node": ">=8"
|
| 1671 |
+
}
|
| 1672 |
+
},
|
| 1673 |
+
"node_modules/jiti": {
|
| 1674 |
+
"version": "2.6.1",
|
| 1675 |
+
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
| 1676 |
+
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
| 1677 |
+
"devOptional": true,
|
| 1678 |
+
"license": "MIT",
|
| 1679 |
+
"bin": {
|
| 1680 |
+
"jiti": "lib/jiti-cli.mjs"
|
| 1681 |
+
}
|
| 1682 |
+
},
|
| 1683 |
+
"node_modules/nanoid": {
|
| 1684 |
+
"version": "3.3.11",
|
| 1685 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 1686 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 1687 |
+
"funding": [
|
| 1688 |
+
{
|
| 1689 |
+
"type": "github",
|
| 1690 |
+
"url": "https://github.com/sponsors/ai"
|
| 1691 |
+
}
|
| 1692 |
+
],
|
| 1693 |
+
"license": "MIT",
|
| 1694 |
+
"bin": {
|
| 1695 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1696 |
+
},
|
| 1697 |
+
"engines": {
|
| 1698 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1699 |
+
}
|
| 1700 |
+
},
|
| 1701 |
+
"node_modules/next": {
|
| 1702 |
+
"version": "15.5.12",
|
| 1703 |
+
"resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz",
|
| 1704 |
+
"integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==",
|
| 1705 |
+
"license": "MIT",
|
| 1706 |
+
"dependencies": {
|
| 1707 |
+
"@next/env": "15.5.12",
|
| 1708 |
+
"@swc/helpers": "0.5.15",
|
| 1709 |
+
"caniuse-lite": "^1.0.30001579",
|
| 1710 |
+
"postcss": "8.4.31",
|
| 1711 |
+
"styled-jsx": "5.1.6"
|
| 1712 |
+
},
|
| 1713 |
+
"bin": {
|
| 1714 |
+
"next": "dist/bin/next"
|
| 1715 |
+
},
|
| 1716 |
+
"engines": {
|
| 1717 |
+
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
| 1718 |
+
},
|
| 1719 |
+
"optionalDependencies": {
|
| 1720 |
+
"@next/swc-darwin-arm64": "15.5.12",
|
| 1721 |
+
"@next/swc-darwin-x64": "15.5.12",
|
| 1722 |
+
"@next/swc-linux-arm64-gnu": "15.5.12",
|
| 1723 |
+
"@next/swc-linux-arm64-musl": "15.5.12",
|
| 1724 |
+
"@next/swc-linux-x64-gnu": "15.5.12",
|
| 1725 |
+
"@next/swc-linux-x64-musl": "15.5.12",
|
| 1726 |
+
"@next/swc-win32-arm64-msvc": "15.5.12",
|
| 1727 |
+
"@next/swc-win32-x64-msvc": "15.5.12",
|
| 1728 |
+
"sharp": "^0.34.3"
|
| 1729 |
+
},
|
| 1730 |
+
"peerDependencies": {
|
| 1731 |
+
"@opentelemetry/api": "^1.1.0",
|
| 1732 |
+
"@playwright/test": "^1.51.1",
|
| 1733 |
+
"babel-plugin-react-compiler": "*",
|
| 1734 |
+
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
| 1735 |
+
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
| 1736 |
+
"sass": "^1.3.0"
|
| 1737 |
+
},
|
| 1738 |
+
"peerDependenciesMeta": {
|
| 1739 |
+
"@opentelemetry/api": {
|
| 1740 |
+
"optional": true
|
| 1741 |
+
},
|
| 1742 |
+
"@playwright/test": {
|
| 1743 |
+
"optional": true
|
| 1744 |
+
},
|
| 1745 |
+
"babel-plugin-react-compiler": {
|
| 1746 |
+
"optional": true
|
| 1747 |
+
},
|
| 1748 |
+
"sass": {
|
| 1749 |
+
"optional": true
|
| 1750 |
+
}
|
| 1751 |
+
}
|
| 1752 |
+
},
|
| 1753 |
+
"node_modules/node-fetch-native": {
|
| 1754 |
+
"version": "1.6.7",
|
| 1755 |
+
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
| 1756 |
+
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
| 1757 |
+
"devOptional": true,
|
| 1758 |
+
"license": "MIT"
|
| 1759 |
+
},
|
| 1760 |
+
"node_modules/nypm": {
|
| 1761 |
+
"version": "0.6.5",
|
| 1762 |
+
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
| 1763 |
+
"integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
|
| 1764 |
+
"devOptional": true,
|
| 1765 |
+
"license": "MIT",
|
| 1766 |
+
"dependencies": {
|
| 1767 |
+
"citty": "^0.2.0",
|
| 1768 |
+
"pathe": "^2.0.3",
|
| 1769 |
+
"tinyexec": "^1.0.2"
|
| 1770 |
+
},
|
| 1771 |
+
"bin": {
|
| 1772 |
+
"nypm": "dist/cli.mjs"
|
| 1773 |
+
},
|
| 1774 |
+
"engines": {
|
| 1775 |
+
"node": ">=18"
|
| 1776 |
+
}
|
| 1777 |
+
},
|
| 1778 |
+
"node_modules/nypm/node_modules/citty": {
|
| 1779 |
+
"version": "0.2.1",
|
| 1780 |
+
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz",
|
| 1781 |
+
"integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==",
|
| 1782 |
+
"devOptional": true,
|
| 1783 |
+
"license": "MIT"
|
| 1784 |
+
},
|
| 1785 |
+
"node_modules/ohash": {
|
| 1786 |
+
"version": "2.0.11",
|
| 1787 |
+
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
| 1788 |
+
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
| 1789 |
+
"devOptional": true,
|
| 1790 |
+
"license": "MIT"
|
| 1791 |
+
},
|
| 1792 |
+
"node_modules/pathe": {
|
| 1793 |
+
"version": "2.0.3",
|
| 1794 |
+
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
| 1795 |
+
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
| 1796 |
+
"devOptional": true,
|
| 1797 |
+
"license": "MIT"
|
| 1798 |
+
},
|
| 1799 |
+
"node_modules/perfect-debounce": {
|
| 1800 |
+
"version": "1.0.0",
|
| 1801 |
+
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
| 1802 |
+
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
| 1803 |
+
"devOptional": true,
|
| 1804 |
+
"license": "MIT"
|
| 1805 |
+
},
|
| 1806 |
+
"node_modules/picocolors": {
|
| 1807 |
+
"version": "1.1.1",
|
| 1808 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1809 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1810 |
+
"license": "ISC"
|
| 1811 |
+
},
|
| 1812 |
+
"node_modules/pkg-types": {
|
| 1813 |
+
"version": "2.3.0",
|
| 1814 |
+
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
| 1815 |
+
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
| 1816 |
+
"devOptional": true,
|
| 1817 |
+
"license": "MIT",
|
| 1818 |
+
"dependencies": {
|
| 1819 |
+
"confbox": "^0.2.2",
|
| 1820 |
+
"exsolve": "^1.0.7",
|
| 1821 |
+
"pathe": "^2.0.3"
|
| 1822 |
+
}
|
| 1823 |
+
},
|
| 1824 |
+
"node_modules/postcss": {
|
| 1825 |
+
"version": "8.4.31",
|
| 1826 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
| 1827 |
+
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
| 1828 |
+
"funding": [
|
| 1829 |
+
{
|
| 1830 |
+
"type": "opencollective",
|
| 1831 |
+
"url": "https://opencollective.com/postcss/"
|
| 1832 |
+
},
|
| 1833 |
+
{
|
| 1834 |
+
"type": "tidelift",
|
| 1835 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1836 |
+
},
|
| 1837 |
+
{
|
| 1838 |
+
"type": "github",
|
| 1839 |
+
"url": "https://github.com/sponsors/ai"
|
| 1840 |
+
}
|
| 1841 |
+
],
|
| 1842 |
+
"license": "MIT",
|
| 1843 |
+
"dependencies": {
|
| 1844 |
+
"nanoid": "^3.3.6",
|
| 1845 |
+
"picocolors": "^1.0.0",
|
| 1846 |
+
"source-map-js": "^1.0.2"
|
| 1847 |
+
},
|
| 1848 |
+
"engines": {
|
| 1849 |
+
"node": "^10 || ^12 || >=14"
|
| 1850 |
+
}
|
| 1851 |
+
},
|
| 1852 |
+
"node_modules/prisma": {
|
| 1853 |
+
"version": "6.19.2",
|
| 1854 |
+
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz",
|
| 1855 |
+
"integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==",
|
| 1856 |
+
"devOptional": true,
|
| 1857 |
+
"hasInstallScript": true,
|
| 1858 |
+
"license": "Apache-2.0",
|
| 1859 |
+
"dependencies": {
|
| 1860 |
+
"@prisma/config": "6.19.2",
|
| 1861 |
+
"@prisma/engines": "6.19.2"
|
| 1862 |
+
},
|
| 1863 |
+
"bin": {
|
| 1864 |
+
"prisma": "build/index.js"
|
| 1865 |
+
},
|
| 1866 |
+
"engines": {
|
| 1867 |
+
"node": ">=18.18"
|
| 1868 |
+
},
|
| 1869 |
+
"peerDependencies": {
|
| 1870 |
+
"typescript": ">=5.1.0"
|
| 1871 |
+
},
|
| 1872 |
+
"peerDependenciesMeta": {
|
| 1873 |
+
"typescript": {
|
| 1874 |
+
"optional": true
|
| 1875 |
+
}
|
| 1876 |
+
}
|
| 1877 |
+
},
|
| 1878 |
+
"node_modules/pure-rand": {
|
| 1879 |
+
"version": "6.1.0",
|
| 1880 |
+
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
| 1881 |
+
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
| 1882 |
+
"devOptional": true,
|
| 1883 |
+
"funding": [
|
| 1884 |
+
{
|
| 1885 |
+
"type": "individual",
|
| 1886 |
+
"url": "https://github.com/sponsors/dubzzz"
|
| 1887 |
+
},
|
| 1888 |
+
{
|
| 1889 |
+
"type": "opencollective",
|
| 1890 |
+
"url": "https://opencollective.com/fast-check"
|
| 1891 |
+
}
|
| 1892 |
+
],
|
| 1893 |
+
"license": "MIT"
|
| 1894 |
+
},
|
| 1895 |
+
"node_modules/rc9": {
|
| 1896 |
+
"version": "2.1.2",
|
| 1897 |
+
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
| 1898 |
+
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
| 1899 |
+
"devOptional": true,
|
| 1900 |
+
"license": "MIT",
|
| 1901 |
+
"dependencies": {
|
| 1902 |
+
"defu": "^6.1.4",
|
| 1903 |
+
"destr": "^2.0.3"
|
| 1904 |
+
}
|
| 1905 |
+
},
|
| 1906 |
+
"node_modules/react": {
|
| 1907 |
+
"version": "19.2.4",
|
| 1908 |
+
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
| 1909 |
+
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
| 1910 |
+
"license": "MIT",
|
| 1911 |
+
"engines": {
|
| 1912 |
+
"node": ">=0.10.0"
|
| 1913 |
+
}
|
| 1914 |
+
},
|
| 1915 |
+
"node_modules/react-dom": {
|
| 1916 |
+
"version": "19.2.4",
|
| 1917 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
| 1918 |
+
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
| 1919 |
+
"license": "MIT",
|
| 1920 |
+
"dependencies": {
|
| 1921 |
+
"scheduler": "^0.27.0"
|
| 1922 |
+
},
|
| 1923 |
+
"peerDependencies": {
|
| 1924 |
+
"react": "^19.2.4"
|
| 1925 |
+
}
|
| 1926 |
+
},
|
| 1927 |
+
"node_modules/readdirp": {
|
| 1928 |
+
"version": "4.1.2",
|
| 1929 |
+
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
| 1930 |
+
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
| 1931 |
+
"devOptional": true,
|
| 1932 |
+
"license": "MIT",
|
| 1933 |
+
"engines": {
|
| 1934 |
+
"node": ">= 14.18.0"
|
| 1935 |
+
},
|
| 1936 |
+
"funding": {
|
| 1937 |
+
"type": "individual",
|
| 1938 |
+
"url": "https://paulmillr.com/funding/"
|
| 1939 |
+
}
|
| 1940 |
+
},
|
| 1941 |
+
"node_modules/require-directory": {
|
| 1942 |
+
"version": "2.1.1",
|
| 1943 |
+
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
| 1944 |
+
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
| 1945 |
+
"dev": true,
|
| 1946 |
+
"license": "MIT",
|
| 1947 |
+
"engines": {
|
| 1948 |
+
"node": ">=0.10.0"
|
| 1949 |
+
}
|
| 1950 |
+
},
|
| 1951 |
+
"node_modules/resolve-pkg-maps": {
|
| 1952 |
+
"version": "1.0.0",
|
| 1953 |
+
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
| 1954 |
+
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
| 1955 |
+
"dev": true,
|
| 1956 |
+
"license": "MIT",
|
| 1957 |
+
"funding": {
|
| 1958 |
+
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
| 1959 |
+
}
|
| 1960 |
+
},
|
| 1961 |
+
"node_modules/rxjs": {
|
| 1962 |
+
"version": "7.8.2",
|
| 1963 |
+
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
| 1964 |
+
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
| 1965 |
+
"dev": true,
|
| 1966 |
+
"license": "Apache-2.0",
|
| 1967 |
+
"dependencies": {
|
| 1968 |
+
"tslib": "^2.1.0"
|
| 1969 |
+
}
|
| 1970 |
+
},
|
| 1971 |
+
"node_modules/scheduler": {
|
| 1972 |
+
"version": "0.27.0",
|
| 1973 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
| 1974 |
+
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
| 1975 |
+
"license": "MIT"
|
| 1976 |
+
},
|
| 1977 |
+
"node_modules/semver": {
|
| 1978 |
+
"version": "7.7.4",
|
| 1979 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
| 1980 |
+
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
| 1981 |
+
"license": "ISC",
|
| 1982 |
+
"optional": true,
|
| 1983 |
+
"bin": {
|
| 1984 |
+
"semver": "bin/semver.js"
|
| 1985 |
+
},
|
| 1986 |
+
"engines": {
|
| 1987 |
+
"node": ">=10"
|
| 1988 |
+
}
|
| 1989 |
+
},
|
| 1990 |
+
"node_modules/sharp": {
|
| 1991 |
+
"version": "0.34.5",
|
| 1992 |
+
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
| 1993 |
+
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
| 1994 |
+
"hasInstallScript": true,
|
| 1995 |
+
"license": "Apache-2.0",
|
| 1996 |
+
"optional": true,
|
| 1997 |
+
"dependencies": {
|
| 1998 |
+
"@img/colour": "^1.0.0",
|
| 1999 |
+
"detect-libc": "^2.1.2",
|
| 2000 |
+
"semver": "^7.7.3"
|
| 2001 |
+
},
|
| 2002 |
+
"engines": {
|
| 2003 |
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
| 2004 |
+
},
|
| 2005 |
+
"funding": {
|
| 2006 |
+
"url": "https://opencollective.com/libvips"
|
| 2007 |
+
},
|
| 2008 |
+
"optionalDependencies": {
|
| 2009 |
+
"@img/sharp-darwin-arm64": "0.34.5",
|
| 2010 |
+
"@img/sharp-darwin-x64": "0.34.5",
|
| 2011 |
+
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
| 2012 |
+
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
| 2013 |
+
"@img/sharp-libvips-linux-arm": "1.2.4",
|
| 2014 |
+
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
| 2015 |
+
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
| 2016 |
+
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
| 2017 |
+
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
| 2018 |
+
"@img/sharp-libvips-linux-x64": "1.2.4",
|
| 2019 |
+
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
| 2020 |
+
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
| 2021 |
+
"@img/sharp-linux-arm": "0.34.5",
|
| 2022 |
+
"@img/sharp-linux-arm64": "0.34.5",
|
| 2023 |
+
"@img/sharp-linux-ppc64": "0.34.5",
|
| 2024 |
+
"@img/sharp-linux-riscv64": "0.34.5",
|
| 2025 |
+
"@img/sharp-linux-s390x": "0.34.5",
|
| 2026 |
+
"@img/sharp-linux-x64": "0.34.5",
|
| 2027 |
+
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
| 2028 |
+
"@img/sharp-linuxmusl-x64": "0.34.5",
|
| 2029 |
+
"@img/sharp-wasm32": "0.34.5",
|
| 2030 |
+
"@img/sharp-win32-arm64": "0.34.5",
|
| 2031 |
+
"@img/sharp-win32-ia32": "0.34.5",
|
| 2032 |
+
"@img/sharp-win32-x64": "0.34.5"
|
| 2033 |
+
}
|
| 2034 |
+
},
|
| 2035 |
+
"node_modules/shell-quote": {
|
| 2036 |
+
"version": "1.8.3",
|
| 2037 |
+
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
| 2038 |
+
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
| 2039 |
+
"dev": true,
|
| 2040 |
+
"license": "MIT",
|
| 2041 |
+
"engines": {
|
| 2042 |
+
"node": ">= 0.4"
|
| 2043 |
+
},
|
| 2044 |
+
"funding": {
|
| 2045 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 2046 |
+
}
|
| 2047 |
+
},
|
| 2048 |
+
"node_modules/source-map-js": {
|
| 2049 |
+
"version": "1.2.1",
|
| 2050 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 2051 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 2052 |
+
"license": "BSD-3-Clause",
|
| 2053 |
+
"engines": {
|
| 2054 |
+
"node": ">=0.10.0"
|
| 2055 |
+
}
|
| 2056 |
+
},
|
| 2057 |
+
"node_modules/string-width": {
|
| 2058 |
+
"version": "4.2.3",
|
| 2059 |
+
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
| 2060 |
+
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
| 2061 |
+
"dev": true,
|
| 2062 |
+
"license": "MIT",
|
| 2063 |
+
"dependencies": {
|
| 2064 |
+
"emoji-regex": "^8.0.0",
|
| 2065 |
+
"is-fullwidth-code-point": "^3.0.0",
|
| 2066 |
+
"strip-ansi": "^6.0.1"
|
| 2067 |
+
},
|
| 2068 |
+
"engines": {
|
| 2069 |
+
"node": ">=8"
|
| 2070 |
+
}
|
| 2071 |
+
},
|
| 2072 |
+
"node_modules/strip-ansi": {
|
| 2073 |
+
"version": "6.0.1",
|
| 2074 |
+
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
| 2075 |
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
| 2076 |
+
"dev": true,
|
| 2077 |
+
"license": "MIT",
|
| 2078 |
+
"dependencies": {
|
| 2079 |
+
"ansi-regex": "^5.0.1"
|
| 2080 |
+
},
|
| 2081 |
+
"engines": {
|
| 2082 |
+
"node": ">=8"
|
| 2083 |
+
}
|
| 2084 |
+
},
|
| 2085 |
+
"node_modules/styled-jsx": {
|
| 2086 |
+
"version": "5.1.6",
|
| 2087 |
+
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
| 2088 |
+
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
|
| 2089 |
+
"license": "MIT",
|
| 2090 |
+
"dependencies": {
|
| 2091 |
+
"client-only": "0.0.1"
|
| 2092 |
+
},
|
| 2093 |
+
"engines": {
|
| 2094 |
+
"node": ">= 12.0.0"
|
| 2095 |
+
},
|
| 2096 |
+
"peerDependencies": {
|
| 2097 |
+
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
|
| 2098 |
+
},
|
| 2099 |
+
"peerDependenciesMeta": {
|
| 2100 |
+
"@babel/core": {
|
| 2101 |
+
"optional": true
|
| 2102 |
+
},
|
| 2103 |
+
"babel-plugin-macros": {
|
| 2104 |
+
"optional": true
|
| 2105 |
+
}
|
| 2106 |
+
}
|
| 2107 |
+
},
|
| 2108 |
+
"node_modules/supports-color": {
|
| 2109 |
+
"version": "8.1.1",
|
| 2110 |
+
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
| 2111 |
+
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
| 2112 |
+
"dev": true,
|
| 2113 |
+
"license": "MIT",
|
| 2114 |
+
"dependencies": {
|
| 2115 |
+
"has-flag": "^4.0.0"
|
| 2116 |
+
},
|
| 2117 |
+
"engines": {
|
| 2118 |
+
"node": ">=10"
|
| 2119 |
+
},
|
| 2120 |
+
"funding": {
|
| 2121 |
+
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
| 2122 |
+
}
|
| 2123 |
+
},
|
| 2124 |
+
"node_modules/tinyexec": {
|
| 2125 |
+
"version": "1.0.2",
|
| 2126 |
+
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
| 2127 |
+
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
| 2128 |
+
"devOptional": true,
|
| 2129 |
+
"license": "MIT",
|
| 2130 |
+
"engines": {
|
| 2131 |
+
"node": ">=18"
|
| 2132 |
+
}
|
| 2133 |
+
},
|
| 2134 |
+
"node_modules/tree-kill": {
|
| 2135 |
+
"version": "1.2.2",
|
| 2136 |
+
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
| 2137 |
+
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
| 2138 |
+
"dev": true,
|
| 2139 |
+
"license": "MIT",
|
| 2140 |
+
"bin": {
|
| 2141 |
+
"tree-kill": "cli.js"
|
| 2142 |
+
}
|
| 2143 |
+
},
|
| 2144 |
+
"node_modules/tslib": {
|
| 2145 |
+
"version": "2.8.1",
|
| 2146 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 2147 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 2148 |
+
"license": "0BSD"
|
| 2149 |
+
},
|
| 2150 |
+
"node_modules/tsx": {
|
| 2151 |
+
"version": "4.21.0",
|
| 2152 |
+
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
| 2153 |
+
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
| 2154 |
+
"dev": true,
|
| 2155 |
+
"license": "MIT",
|
| 2156 |
+
"dependencies": {
|
| 2157 |
+
"esbuild": "~0.27.0",
|
| 2158 |
+
"get-tsconfig": "^4.7.5"
|
| 2159 |
+
},
|
| 2160 |
+
"bin": {
|
| 2161 |
+
"tsx": "dist/cli.mjs"
|
| 2162 |
+
},
|
| 2163 |
+
"engines": {
|
| 2164 |
+
"node": ">=18.0.0"
|
| 2165 |
+
},
|
| 2166 |
+
"optionalDependencies": {
|
| 2167 |
+
"fsevents": "~2.3.3"
|
| 2168 |
+
}
|
| 2169 |
+
},
|
| 2170 |
+
"node_modules/typescript": {
|
| 2171 |
+
"version": "5.9.3",
|
| 2172 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 2173 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 2174 |
+
"devOptional": true,
|
| 2175 |
+
"license": "Apache-2.0",
|
| 2176 |
+
"bin": {
|
| 2177 |
+
"tsc": "bin/tsc",
|
| 2178 |
+
"tsserver": "bin/tsserver"
|
| 2179 |
+
},
|
| 2180 |
+
"engines": {
|
| 2181 |
+
"node": ">=14.17"
|
| 2182 |
+
}
|
| 2183 |
+
},
|
| 2184 |
+
"node_modules/undici-types": {
|
| 2185 |
+
"version": "6.21.0",
|
| 2186 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 2187 |
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 2188 |
+
"dev": true,
|
| 2189 |
+
"license": "MIT"
|
| 2190 |
+
},
|
| 2191 |
+
"node_modules/wrap-ansi": {
|
| 2192 |
+
"version": "7.0.0",
|
| 2193 |
+
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
| 2194 |
+
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
| 2195 |
+
"dev": true,
|
| 2196 |
+
"license": "MIT",
|
| 2197 |
+
"dependencies": {
|
| 2198 |
+
"ansi-styles": "^4.0.0",
|
| 2199 |
+
"string-width": "^4.1.0",
|
| 2200 |
+
"strip-ansi": "^6.0.0"
|
| 2201 |
+
},
|
| 2202 |
+
"engines": {
|
| 2203 |
+
"node": ">=10"
|
| 2204 |
+
},
|
| 2205 |
+
"funding": {
|
| 2206 |
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
| 2207 |
+
}
|
| 2208 |
+
},
|
| 2209 |
+
"node_modules/y18n": {
|
| 2210 |
+
"version": "5.0.8",
|
| 2211 |
+
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
| 2212 |
+
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
| 2213 |
+
"dev": true,
|
| 2214 |
+
"license": "ISC",
|
| 2215 |
+
"engines": {
|
| 2216 |
+
"node": ">=10"
|
| 2217 |
+
}
|
| 2218 |
+
},
|
| 2219 |
+
"node_modules/yargs": {
|
| 2220 |
+
"version": "17.7.2",
|
| 2221 |
+
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
| 2222 |
+
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
| 2223 |
+
"dev": true,
|
| 2224 |
+
"license": "MIT",
|
| 2225 |
+
"dependencies": {
|
| 2226 |
+
"cliui": "^8.0.1",
|
| 2227 |
+
"escalade": "^3.1.1",
|
| 2228 |
+
"get-caller-file": "^2.0.5",
|
| 2229 |
+
"require-directory": "^2.1.1",
|
| 2230 |
+
"string-width": "^4.2.3",
|
| 2231 |
+
"y18n": "^5.0.5",
|
| 2232 |
+
"yargs-parser": "^21.1.1"
|
| 2233 |
+
},
|
| 2234 |
+
"engines": {
|
| 2235 |
+
"node": ">=12"
|
| 2236 |
+
}
|
| 2237 |
+
},
|
| 2238 |
+
"node_modules/yargs-parser": {
|
| 2239 |
+
"version": "21.1.1",
|
| 2240 |
+
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
| 2241 |
+
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
| 2242 |
+
"dev": true,
|
| 2243 |
+
"license": "ISC",
|
| 2244 |
+
"engines": {
|
| 2245 |
+
"node": ">=12"
|
| 2246 |
+
}
|
| 2247 |
+
}
|
| 2248 |
+
}
|
| 2249 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "ehrgym",
|
| 3 |
+
"private": true,
|
| 4 |
+
"type": "module",
|
| 5 |
+
"workspaces": [
|
| 6 |
+
"apps/ehr"
|
| 7 |
+
],
|
| 8 |
+
"scripts": {
|
| 9 |
+
"dev": "concurrently -k -n ehr,env \"npm run dev:ehr\" \"npm run dev:env\"",
|
| 10 |
+
"dev:ehr": "npm run dev --workspace @ehrgym/ehr",
|
| 11 |
+
"dev:env": "python3 -m uvicorn env_server.app.main:app --reload --host 0.0.0.0 --port 8000",
|
| 12 |
+
"clean:ehr": "npm run clean --workspace @ehrgym/ehr",
|
| 13 |
+
"build:ehr": "npm run build --workspace @ehrgym/ehr",
|
| 14 |
+
"typecheck": "npm run typecheck --workspace @ehrgym/ehr",
|
| 15 |
+
"db:generate": "prisma generate",
|
| 16 |
+
"db:push": "prisma db push",
|
| 17 |
+
"db:seed": "prisma db seed"
|
| 18 |
+
},
|
| 19 |
+
"prisma": {
|
| 20 |
+
"seed": "tsx prisma/seed.ts"
|
| 21 |
+
},
|
| 22 |
+
"devDependencies": {
|
| 23 |
+
"@types/node": "^22.13.14",
|
| 24 |
+
"concurrently": "^9.1.2",
|
| 25 |
+
"prisma": "^6.5.0",
|
| 26 |
+
"tsx": "^4.19.3",
|
| 27 |
+
"typescript": "^5.8.2"
|
| 28 |
+
}
|
| 29 |
+
}
|
prisma/schema.prisma
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generator client {
|
| 2 |
+
provider = "prisma-client-js"
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
datasource db {
|
| 6 |
+
provider = "sqlite"
|
| 7 |
+
url = env("DATABASE_URL")
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
enum EncounterStatus {
|
| 11 |
+
OPEN
|
| 12 |
+
SIGNED
|
| 13 |
+
CLOSED
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
enum NoteType {
|
| 17 |
+
PROGRESS
|
| 18 |
+
CONSULT
|
| 19 |
+
DISCHARGE
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
enum OrderCategory {
|
| 23 |
+
LAB
|
| 24 |
+
MED
|
| 25 |
+
IMAGING
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
enum OrderStatus {
|
| 29 |
+
DRAFT
|
| 30 |
+
PENDING_SIGNATURE
|
| 31 |
+
SIGNED
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
model Patient {
|
| 35 |
+
id String @id
|
| 36 |
+
mrn String @unique
|
| 37 |
+
fullName String
|
| 38 |
+
age Int
|
| 39 |
+
sex String
|
| 40 |
+
allergiesJson String
|
| 41 |
+
bannerFlagsJson String
|
| 42 |
+
summary String
|
| 43 |
+
createdAt DateTime @default(now())
|
| 44 |
+
updatedAt DateTime @updatedAt
|
| 45 |
+
encounters Encounter[]
|
| 46 |
+
scenarios Scenario[]
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
model Encounter {
|
| 50 |
+
id String @id
|
| 51 |
+
patientId String
|
| 52 |
+
type String
|
| 53 |
+
reasonForVisit String
|
| 54 |
+
provider String
|
| 55 |
+
startedAt DateTime
|
| 56 |
+
status EncounterStatus @default(OPEN)
|
| 57 |
+
createdAt DateTime @default(now())
|
| 58 |
+
updatedAt DateTime @updatedAt
|
| 59 |
+
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
| 60 |
+
labs LabResult[]
|
| 61 |
+
notes ClinicalNote[]
|
| 62 |
+
orders Order[]
|
| 63 |
+
scenarios Scenario[]
|
| 64 |
+
|
| 65 |
+
@@index([patientId, startedAt])
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
model LabResult {
|
| 69 |
+
id String @id
|
| 70 |
+
encounterId String
|
| 71 |
+
name String
|
| 72 |
+
loinc String?
|
| 73 |
+
value String
|
| 74 |
+
unit String
|
| 75 |
+
referenceRange String
|
| 76 |
+
abnormal Boolean @default(false)
|
| 77 |
+
collectedAt DateTime
|
| 78 |
+
encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade)
|
| 79 |
+
|
| 80 |
+
@@index([encounterId, collectedAt])
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
model ClinicalNote {
|
| 84 |
+
id String @id
|
| 85 |
+
encounterId String
|
| 86 |
+
type NoteType
|
| 87 |
+
title String
|
| 88 |
+
author String
|
| 89 |
+
content String
|
| 90 |
+
signed Boolean @default(false)
|
| 91 |
+
createdAt DateTime @default(now())
|
| 92 |
+
encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade)
|
| 93 |
+
|
| 94 |
+
@@index([encounterId, createdAt])
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
model Order {
|
| 98 |
+
id String @id
|
| 99 |
+
encounterId String
|
| 100 |
+
name String
|
| 101 |
+
category OrderCategory
|
| 102 |
+
parametersJson String
|
| 103 |
+
status OrderStatus @default(DRAFT)
|
| 104 |
+
rationale String
|
| 105 |
+
createdAt DateTime @default(now())
|
| 106 |
+
encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade)
|
| 107 |
+
|
| 108 |
+
@@index([encounterId, createdAt])
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
model Scenario {
|
| 112 |
+
id String @id
|
| 113 |
+
patientId String
|
| 114 |
+
encounterId String
|
| 115 |
+
title String
|
| 116 |
+
objective String
|
| 117 |
+
rubricJson String
|
| 118 |
+
requiredOrdersJson String
|
| 119 |
+
requiredNoteElementsJson String
|
| 120 |
+
createdAt DateTime @default(now())
|
| 121 |
+
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
| 122 |
+
encounter Encounter @relation(fields: [encounterId], references: [id], onDelete: Cascade)
|
| 123 |
+
|
| 124 |
+
@@index([patientId, encounterId])
|
| 125 |
+
}
|
prisma/seed.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PrismaClient } from "@prisma/client";
|
| 2 |
+
|
| 3 |
+
import { resetDatabase } from "../shared/reset-database";
|
| 4 |
+
|
| 5 |
+
const prisma = new PrismaClient();
|
| 6 |
+
|
| 7 |
+
async function main() {
|
| 8 |
+
await resetDatabase(prisma);
|
| 9 |
+
const patientCount = await prisma.patient.count();
|
| 10 |
+
console.log(`Seeded ${patientCount} synthetic patients.`);
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
main()
|
| 14 |
+
.catch((error) => {
|
| 15 |
+
console.error("Failed to seed database", error);
|
| 16 |
+
process.exitCode = 1;
|
| 17 |
+
})
|
| 18 |
+
.finally(async () => {
|
| 19 |
+
await prisma.$disconnect();
|
| 20 |
+
});
|
pyproject.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=69", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "ehrgym-env-server"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "OpenEnv-style FastAPI + Playwright environment server for EHRGym"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.9"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"fastapi>=0.115.0",
|
| 13 |
+
"httpx>=0.28.1",
|
| 14 |
+
"playwright>=1.51.0",
|
| 15 |
+
"pydantic>=2.10.6",
|
| 16 |
+
"uvicorn[standard]>=0.34.0"
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[tool.setuptools]
|
| 20 |
+
include-package-data = true
|
| 21 |
+
|
| 22 |
+
[tool.setuptools.packages.find]
|
| 23 |
+
include = ["env_server*"]
|
scripts/diagnose_server_actions.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
|
| 5 |
+
from playwright.async_api import async_playwright
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def main() -> None:
|
| 9 |
+
async with async_playwright() as playwright:
|
| 10 |
+
browser = await playwright.chromium.launch()
|
| 11 |
+
page = await browser.new_page()
|
| 12 |
+
events: list[tuple[str, str, int] | tuple[str, str, str]] = []
|
| 13 |
+
|
| 14 |
+
page.on(
|
| 15 |
+
"response",
|
| 16 |
+
lambda response: events.append((response.request.method, response.url, response.status))
|
| 17 |
+
if response.request.method == "POST"
|
| 18 |
+
else None,
|
| 19 |
+
)
|
| 20 |
+
page.on(
|
| 21 |
+
"console",
|
| 22 |
+
lambda message: events.append(("console", message.type, message.text)) if message.type == "error" else None,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
await page.goto("http://127.0.0.1:3000/patient/pat-1001", wait_until="networkidle")
|
| 26 |
+
await page.get_by_label("Note author").fill("Dr. Test User")
|
| 27 |
+
await page.get_by_label("Note title").fill("Progress Note Follow-up")
|
| 28 |
+
await page.get_by_label("Progress note content").fill("S\nO\nA\nP")
|
| 29 |
+
await page.get_by_test_id("save-note-button").click()
|
| 30 |
+
await page.wait_for_timeout(1500)
|
| 31 |
+
|
| 32 |
+
print("events", events)
|
| 33 |
+
print("url", page.url)
|
| 34 |
+
print("count", await page.get_by_text("Progress Note Follow-up").count())
|
| 35 |
+
|
| 36 |
+
await browser.close()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
asyncio.run(main())
|
scripts/example_agent.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
import httpx
|
| 6 |
+
|
| 7 |
+
ENV_SERVER_URL = "http://127.0.0.1:8000"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main() -> None:
|
| 11 |
+
with httpx.Client(base_url=ENV_SERVER_URL, timeout=30.0) as client:
|
| 12 |
+
reset = client.post("/reset").json()
|
| 13 |
+
print("Goal:", reset["observation"]["goal"])
|
| 14 |
+
print("State:", json.dumps(reset["state"], indent=2))
|
| 15 |
+
|
| 16 |
+
actions = [
|
| 17 |
+
{"type": "click", "selector": "[data-testid='patient-card-pat-1001']"},
|
| 18 |
+
{"type": "wait", "milliseconds": 500},
|
| 19 |
+
{"type": "click", "selector": "[data-testid='activity-orders']"}
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
for action in actions:
|
| 23 |
+
step = client.post("/step", json=action).json()
|
| 24 |
+
print("Action:", action)
|
| 25 |
+
print("Reward:", step["reward"])
|
| 26 |
+
print("Done:", step["done"])
|
| 27 |
+
print("Progress:", step["state"]["rubric_progress"])
|
| 28 |
+
print("URL:", step["observation"]["current_url"])
|
| 29 |
+
print("-" * 40)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
main()
|
scripts/measure_controls.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
|
| 5 |
+
from playwright.async_api import async_playwright
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def main() -> None:
|
| 9 |
+
async with async_playwright() as playwright:
|
| 10 |
+
browser = await playwright.chromium.launch()
|
| 11 |
+
page = await browser.new_page()
|
| 12 |
+
await page.goto("http://127.0.0.1:3000/patient/pat-1001", wait_until="networkidle")
|
| 13 |
+
|
| 14 |
+
checkbox_box = await page.get_by_label("Submit order for signature").bounding_box()
|
| 15 |
+
order_button_box = await page.get_by_test_id("save-order-button").bounding_box()
|
| 16 |
+
note_button_box = await page.get_by_test_id("save-note-button").bounding_box()
|
| 17 |
+
|
| 18 |
+
print("checkbox", checkbox_box)
|
| 19 |
+
print("order_button", order_button_box)
|
| 20 |
+
print("note_button", note_button_box)
|
| 21 |
+
|
| 22 |
+
await browser.close()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
if __name__ == "__main__":
|
| 26 |
+
asyncio.run(main())
|
scripts/ui_smoke_test.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
from time import time
|
| 5 |
+
|
| 6 |
+
from playwright.async_api import async_playwright
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
async def main() -> None:
|
| 10 |
+
async with async_playwright() as playwright:
|
| 11 |
+
browser = await playwright.chromium.launch()
|
| 12 |
+
page = await browser.new_page()
|
| 13 |
+
messages: list[str] = []
|
| 14 |
+
|
| 15 |
+
page.on("pageerror", lambda error: messages.append(f"PAGEERROR: {error}"))
|
| 16 |
+
page.on(
|
| 17 |
+
"console",
|
| 18 |
+
lambda message: messages.append(f"CONSOLE {message.type}: {message.text}") if message.type == "error" else None,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
suffix = str(int(time()))
|
| 22 |
+
note_title = f"Progress Note Follow-up {suffix}"
|
| 23 |
+
order_name = f"Normal saline bolus {suffix}"
|
| 24 |
+
|
| 25 |
+
await page.goto("http://127.0.0.1:3000/patient/pat-1001", wait_until="networkidle")
|
| 26 |
+
print("loaded", page.url)
|
| 27 |
+
|
| 28 |
+
await page.get_by_test_id("chart-tab-labs").click()
|
| 29 |
+
print("labs_tab_active", await page.get_by_test_id("chart-tab-labs").get_attribute("aria-selected"))
|
| 30 |
+
await page.get_by_test_id("chart-tab-notes").click()
|
| 31 |
+
print("chart_notes_tab_active", await page.get_by_test_id("chart-tab-notes").get_attribute("aria-selected"))
|
| 32 |
+
await page.get_by_test_id("activity-orders").click()
|
| 33 |
+
await page.wait_for_timeout(150)
|
| 34 |
+
print("hash_after_orders_nav", await page.evaluate("window.location.hash"))
|
| 35 |
+
|
| 36 |
+
await page.get_by_label("Note author").fill("Dr. Test User")
|
| 37 |
+
await page.get_by_label("Note title").fill(note_title)
|
| 38 |
+
await page.get_by_label("Progress note content").fill(
|
| 39 |
+
"S: feels better\nO: creatinine trend reviewed\nA: volume depletion with AKI\nP: repeat BMP and volume assessment"
|
| 40 |
+
)
|
| 41 |
+
await page.get_by_test_id("save-note-button").click()
|
| 42 |
+
await page.locator("article.note-row", has_text=note_title).last.wait_for(timeout=5000)
|
| 43 |
+
print("has_new_note", await page.locator("article.note-row", has_text=note_title).count())
|
| 44 |
+
|
| 45 |
+
await page.get_by_label("Order name").fill(order_name)
|
| 46 |
+
await page.get_by_label("Order category").select_option("MED")
|
| 47 |
+
await page.get_by_label("Order parameters").fill("1 L IV once")
|
| 48 |
+
await page.get_by_label("Order rationale").fill("Volume repletion for AKI")
|
| 49 |
+
await page.get_by_label("Submit order for signature").check()
|
| 50 |
+
await page.get_by_test_id("save-order-button").click()
|
| 51 |
+
await page.locator("article.order-row", has_text=order_name).first.wait_for(timeout=5000)
|
| 52 |
+
print("has_new_order", await page.locator("article.order-row", has_text=order_name).count())
|
| 53 |
+
|
| 54 |
+
new_order_row = page.locator("article.order-row", has_text=order_name).first
|
| 55 |
+
sign_buttons = new_order_row.locator('[data-testid^="sign-order-"]')
|
| 56 |
+
print("sign_buttons", await sign_buttons.count())
|
| 57 |
+
if await sign_buttons.count() > 0:
|
| 58 |
+
await sign_buttons.first.click()
|
| 59 |
+
await new_order_row.get_by_text("SIGNED").wait_for(timeout=5000)
|
| 60 |
+
print("signed_clicked", True)
|
| 61 |
+
|
| 62 |
+
await page.get_by_test_id("sign-encounter-button").click()
|
| 63 |
+
await page.locator('[data-testid="patient-banner"] .status-pill').get_by_text("SIGNED").wait_for(timeout=5000)
|
| 64 |
+
print("encounter_status", await page.locator('[data-testid="patient-banner"] .status-pill').inner_text())
|
| 65 |
+
|
| 66 |
+
if messages:
|
| 67 |
+
for message in messages:
|
| 68 |
+
print(message)
|
| 69 |
+
|
| 70 |
+
await browser.close()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
asyncio.run(main())
|
shared/reset-database.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { PrismaClient } from "@prisma/client";
|
| 2 |
+
|
| 3 |
+
import { seedPatients } from "./seed-data";
|
| 4 |
+
|
| 5 |
+
export async function resetDatabase(prisma: PrismaClient) {
|
| 6 |
+
await prisma.order.deleteMany();
|
| 7 |
+
await prisma.clinicalNote.deleteMany();
|
| 8 |
+
await prisma.labResult.deleteMany();
|
| 9 |
+
await prisma.scenario.deleteMany();
|
| 10 |
+
await prisma.encounter.deleteMany();
|
| 11 |
+
await prisma.patient.deleteMany();
|
| 12 |
+
|
| 13 |
+
for (const patient of seedPatients) {
|
| 14 |
+
await prisma.patient.create({
|
| 15 |
+
data: {
|
| 16 |
+
id: patient.id,
|
| 17 |
+
mrn: patient.mrn,
|
| 18 |
+
fullName: patient.fullName,
|
| 19 |
+
age: patient.age,
|
| 20 |
+
sex: patient.sex,
|
| 21 |
+
allergiesJson: JSON.stringify(patient.allergies),
|
| 22 |
+
bannerFlagsJson: JSON.stringify(patient.bannerFlags),
|
| 23 |
+
summary: patient.summary,
|
| 24 |
+
encounters: {
|
| 25 |
+
create: patient.encounters.map((encounter) => ({
|
| 26 |
+
id: encounter.id,
|
| 27 |
+
type: encounter.type,
|
| 28 |
+
reasonForVisit: encounter.reasonForVisit,
|
| 29 |
+
provider: encounter.provider,
|
| 30 |
+
startedAt: new Date(encounter.startedAt),
|
| 31 |
+
status: encounter.status,
|
| 32 |
+
labs: {
|
| 33 |
+
create: encounter.labs.map((lab) => ({
|
| 34 |
+
id: lab.id,
|
| 35 |
+
name: lab.name,
|
| 36 |
+
loinc: lab.loinc,
|
| 37 |
+
value: lab.value,
|
| 38 |
+
unit: lab.unit,
|
| 39 |
+
referenceRange: lab.referenceRange,
|
| 40 |
+
abnormal: lab.abnormal,
|
| 41 |
+
collectedAt: new Date(lab.collectedAt)
|
| 42 |
+
}))
|
| 43 |
+
},
|
| 44 |
+
notes: {
|
| 45 |
+
create: encounter.notes.map((note) => ({
|
| 46 |
+
id: note.id,
|
| 47 |
+
type: note.type,
|
| 48 |
+
title: note.title,
|
| 49 |
+
author: note.author,
|
| 50 |
+
content: note.content,
|
| 51 |
+
signed: note.signed,
|
| 52 |
+
createdAt: new Date(note.createdAt)
|
| 53 |
+
}))
|
| 54 |
+
},
|
| 55 |
+
orders: {
|
| 56 |
+
create: encounter.orders.map((order) => ({
|
| 57 |
+
id: order.id,
|
| 58 |
+
name: order.name,
|
| 59 |
+
category: order.category,
|
| 60 |
+
parametersJson: JSON.stringify(order.parameters),
|
| 61 |
+
status: order.status,
|
| 62 |
+
rationale: order.rationale,
|
| 63 |
+
createdAt: new Date(order.createdAt)
|
| 64 |
+
}))
|
| 65 |
+
}
|
| 66 |
+
}))
|
| 67 |
+
},
|
| 68 |
+
scenarios: {
|
| 69 |
+
create: patient.scenarios.map((scenario, index) => ({
|
| 70 |
+
id: scenario.id,
|
| 71 |
+
title: scenario.title,
|
| 72 |
+
objective: scenario.objective,
|
| 73 |
+
rubricJson: JSON.stringify(scenario.rubric),
|
| 74 |
+
requiredOrdersJson: JSON.stringify(scenario.requiredOrders),
|
| 75 |
+
requiredNoteElementsJson: JSON.stringify(scenario.requiredNoteElements),
|
| 76 |
+
encounterId: patient.encounters[index]?.id ?? patient.encounters[0]?.id
|
| 77 |
+
}))
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
}
|
shared/seed-data.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type SeedLab = {
|
| 2 |
+
id: string;
|
| 3 |
+
name: string;
|
| 4 |
+
loinc?: string;
|
| 5 |
+
value: string;
|
| 6 |
+
unit: string;
|
| 7 |
+
referenceRange: string;
|
| 8 |
+
abnormal: boolean;
|
| 9 |
+
collectedAt: string;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
export type SeedNote = {
|
| 13 |
+
id: string;
|
| 14 |
+
type: "PROGRESS" | "CONSULT" | "DISCHARGE";
|
| 15 |
+
title: string;
|
| 16 |
+
author: string;
|
| 17 |
+
content: string;
|
| 18 |
+
signed: boolean;
|
| 19 |
+
createdAt: string;
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
export type SeedOrder = {
|
| 23 |
+
id: string;
|
| 24 |
+
name: string;
|
| 25 |
+
category: "LAB" | "MED" | "IMAGING";
|
| 26 |
+
parameters: Record<string, string>;
|
| 27 |
+
status: "DRAFT" | "PENDING_SIGNATURE" | "SIGNED";
|
| 28 |
+
rationale: string;
|
| 29 |
+
createdAt: string;
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
export type SeedEncounter = {
|
| 33 |
+
id: string;
|
| 34 |
+
type: string;
|
| 35 |
+
reasonForVisit: string;
|
| 36 |
+
provider: string;
|
| 37 |
+
startedAt: string;
|
| 38 |
+
status: "OPEN" | "SIGNED" | "CLOSED";
|
| 39 |
+
labs: SeedLab[];
|
| 40 |
+
notes: SeedNote[];
|
| 41 |
+
orders: SeedOrder[];
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
export type SeedScenario = {
|
| 45 |
+
id: string;
|
| 46 |
+
title: string;
|
| 47 |
+
objective: string;
|
| 48 |
+
rubric: string[];
|
| 49 |
+
requiredOrders: string[];
|
| 50 |
+
requiredNoteElements: string[];
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
export type SeedPatient = {
|
| 54 |
+
id: string;
|
| 55 |
+
mrn: string;
|
| 56 |
+
fullName: string;
|
| 57 |
+
age: number;
|
| 58 |
+
sex: string;
|
| 59 |
+
allergies: string[];
|
| 60 |
+
bannerFlags: string[];
|
| 61 |
+
summary: string;
|
| 62 |
+
encounters: SeedEncounter[];
|
| 63 |
+
scenarios: SeedScenario[];
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
export const seedPatients: SeedPatient[] = [
|
| 67 |
+
{
|
| 68 |
+
id: "pat-1001",
|
| 69 |
+
mrn: "SYN-1001",
|
| 70 |
+
fullName: "Mary Johnson",
|
| 71 |
+
age: 68,
|
| 72 |
+
sex: "F",
|
| 73 |
+
allergies: ["Penicillin"],
|
| 74 |
+
bannerFlags: ["Fall risk", "CKD stage 3"],
|
| 75 |
+
summary: "Admitted with weakness and oliguria after several days of poor oral intake.",
|
| 76 |
+
encounters: [
|
| 77 |
+
{
|
| 78 |
+
id: "enc-1001",
|
| 79 |
+
type: "Inpatient",
|
| 80 |
+
reasonForVisit: "Acute kidney injury evaluation",
|
| 81 |
+
provider: "Dr. Ada Carter",
|
| 82 |
+
startedAt: "2026-03-05T14:15:00.000Z",
|
| 83 |
+
status: "OPEN",
|
| 84 |
+
labs: [
|
| 85 |
+
{
|
| 86 |
+
id: "lab-1001",
|
| 87 |
+
name: "Creatinine",
|
| 88 |
+
loinc: "2160-0",
|
| 89 |
+
value: "2.3",
|
| 90 |
+
unit: "mg/dL",
|
| 91 |
+
referenceRange: "0.6-1.2",
|
| 92 |
+
abnormal: true,
|
| 93 |
+
collectedAt: "2026-03-06T08:12:00.000Z"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
id: "lab-1002",
|
| 97 |
+
name: "Creatinine",
|
| 98 |
+
loinc: "2160-0",
|
| 99 |
+
value: "1.8",
|
| 100 |
+
unit: "mg/dL",
|
| 101 |
+
referenceRange: "0.6-1.2",
|
| 102 |
+
abnormal: true,
|
| 103 |
+
collectedAt: "2026-03-05T09:02:00.000Z"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
id: "lab-1003",
|
| 107 |
+
name: "Hemoglobin",
|
| 108 |
+
loinc: "718-7",
|
| 109 |
+
value: "10.4",
|
| 110 |
+
unit: "g/dL",
|
| 111 |
+
referenceRange: "12.0-16.0",
|
| 112 |
+
abnormal: true,
|
| 113 |
+
collectedAt: "2026-03-06T08:12:00.000Z"
|
| 114 |
+
}
|
| 115 |
+
],
|
| 116 |
+
notes: [
|
| 117 |
+
{
|
| 118 |
+
id: "note-1001",
|
| 119 |
+
type: "CONSULT",
|
| 120 |
+
title: "Nephrology consult",
|
| 121 |
+
author: "Dr. J. Morales",
|
| 122 |
+
content: "Assessment: pre-renal AKI suspected. Recommend isotonic fluids, medication review, and repeat BMP in 6 hours.",
|
| 123 |
+
signed: true,
|
| 124 |
+
createdAt: "2026-03-05T16:40:00.000Z"
|
| 125 |
+
}
|
| 126 |
+
],
|
| 127 |
+
orders: [
|
| 128 |
+
{
|
| 129 |
+
id: "ord-1001",
|
| 130 |
+
name: "Basic metabolic panel",
|
| 131 |
+
category: "LAB",
|
| 132 |
+
parameters: {
|
| 133 |
+
frequency: "q6h"
|
| 134 |
+
},
|
| 135 |
+
status: "SIGNED",
|
| 136 |
+
rationale: "Trend renal function.",
|
| 137 |
+
createdAt: "2026-03-05T16:55:00.000Z"
|
| 138 |
+
}
|
| 139 |
+
]
|
| 140 |
+
}
|
| 141 |
+
],
|
| 142 |
+
scenarios: [
|
| 143 |
+
{
|
| 144 |
+
id: "scn-1001",
|
| 145 |
+
title: "AKI chart review",
|
| 146 |
+
objective: "Review labs, place fluid and repeat BMP orders, then write a grounded SOAP progress note.",
|
| 147 |
+
rubric: [
|
| 148 |
+
"Identify most recent creatinine",
|
| 149 |
+
"Order isotonic fluids or repeat BMP",
|
| 150 |
+
"Document likely pre-renal AKI in assessment",
|
| 151 |
+
"Sign the encounter"
|
| 152 |
+
],
|
| 153 |
+
requiredOrders: ["Basic metabolic panel", "Normal saline bolus"],
|
| 154 |
+
requiredNoteElements: ["Creatinine trend", "Volume assessment", "Plan for repeat labs"]
|
| 155 |
+
}
|
| 156 |
+
]
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
id: "pat-1002",
|
| 160 |
+
mrn: "SYN-1002",
|
| 161 |
+
fullName: "Robert Chen",
|
| 162 |
+
age: 52,
|
| 163 |
+
sex: "M",
|
| 164 |
+
allergies: ["None known"],
|
| 165 |
+
bannerFlags: ["Droplet precautions"],
|
| 166 |
+
summary: "Seen for fever, productive cough, and exertional dyspnea.",
|
| 167 |
+
encounters: [
|
| 168 |
+
{
|
| 169 |
+
id: "enc-1002",
|
| 170 |
+
type: "Observation",
|
| 171 |
+
reasonForVisit: "Community-acquired pneumonia",
|
| 172 |
+
provider: "Dr. Nina Brooks",
|
| 173 |
+
startedAt: "2026-03-04T11:05:00.000Z",
|
| 174 |
+
status: "OPEN",
|
| 175 |
+
labs: [
|
| 176 |
+
{
|
| 177 |
+
id: "lab-2001",
|
| 178 |
+
name: "WBC",
|
| 179 |
+
loinc: "6690-2",
|
| 180 |
+
value: "14.8",
|
| 181 |
+
unit: "K/uL",
|
| 182 |
+
referenceRange: "4.0-10.5",
|
| 183 |
+
abnormal: true,
|
| 184 |
+
collectedAt: "2026-03-04T11:40:00.000Z"
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
id: "lab-2002",
|
| 188 |
+
name: "Procalcitonin",
|
| 189 |
+
loinc: "33959-8",
|
| 190 |
+
value: "1.8",
|
| 191 |
+
unit: "ng/mL",
|
| 192 |
+
referenceRange: "<0.5",
|
| 193 |
+
abnormal: true,
|
| 194 |
+
collectedAt: "2026-03-04T11:40:00.000Z"
|
| 195 |
+
}
|
| 196 |
+
],
|
| 197 |
+
notes: [
|
| 198 |
+
{
|
| 199 |
+
id: "note-2001",
|
| 200 |
+
type: "DISCHARGE",
|
| 201 |
+
title: "Prior urgent care note",
|
| 202 |
+
author: "Dr. A. Singh",
|
| 203 |
+
content: "Started doxycycline yesterday; advised chest imaging if symptoms worsen.",
|
| 204 |
+
signed: true,
|
| 205 |
+
createdAt: "2026-03-03T18:20:00.000Z"
|
| 206 |
+
}
|
| 207 |
+
],
|
| 208 |
+
orders: [
|
| 209 |
+
{
|
| 210 |
+
id: "ord-2001",
|
| 211 |
+
name: "Chest X-ray",
|
| 212 |
+
category: "IMAGING",
|
| 213 |
+
parameters: {
|
| 214 |
+
priority: "Routine"
|
| 215 |
+
},
|
| 216 |
+
status: "PENDING_SIGNATURE",
|
| 217 |
+
rationale: "Evaluate infiltrate.",
|
| 218 |
+
createdAt: "2026-03-04T11:50:00.000Z"
|
| 219 |
+
}
|
| 220 |
+
]
|
| 221 |
+
}
|
| 222 |
+
],
|
| 223 |
+
scenarios: [
|
| 224 |
+
{
|
| 225 |
+
id: "scn-1002",
|
| 226 |
+
title: "Pneumonia follow-up",
|
| 227 |
+
objective: "Review infectious workup, document prior antibiotic exposure, and sign a chest X-ray order.",
|
| 228 |
+
rubric: [
|
| 229 |
+
"Find prior antibiotic exposure",
|
| 230 |
+
"Place or sign chest imaging order",
|
| 231 |
+
"Write a concise progress note"
|
| 232 |
+
],
|
| 233 |
+
requiredOrders: ["Chest X-ray"],
|
| 234 |
+
requiredNoteElements: ["Antibiotic exposure", "Respiratory symptoms", "Follow-up plan"]
|
| 235 |
+
}
|
| 236 |
+
]
|
| 237 |
+
}
|
| 238 |
+
];
|
synthetic/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Synthetic data pipeline
|
| 2 |
+
|
| 3 |
+
This folder is reserved for Synthea generation, FHIR-shaped ingest, and repeatable seed tooling.
|
| 4 |
+
|
| 5 |
+
For the initial scaffold, synthetic chart data is defined in [shared/seed-data.ts](../shared/seed-data.ts) and loaded through Prisma.
|
tasks/examples/aki-chart-review.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "aki-chart-review",
|
| 3 |
+
"title": "AKI chart review to note and orders",
|
| 4 |
+
"patient_id": "pat-1001",
|
| 5 |
+
"encounter_id": "enc-1001",
|
| 6 |
+
"objective": "Review the patient chart, identify the rising creatinine, place fluid and repeat BMP orders, then sign the encounter.",
|
| 7 |
+
"required_orders": [
|
| 8 |
+
"Basic metabolic panel",
|
| 9 |
+
"Normal saline bolus"
|
| 10 |
+
],
|
| 11 |
+
"required_note_elements": [
|
| 12 |
+
"Creatinine trend",
|
| 13 |
+
"Volume assessment",
|
| 14 |
+
"Plan for repeat labs"
|
| 15 |
+
],
|
| 16 |
+
"scoring": {
|
| 17 |
+
"base_reward": 1.0,
|
| 18 |
+
"substeps": {
|
| 19 |
+
"chart_navigation": 0.1,
|
| 20 |
+
"target_lab_identified": 0.2,
|
| 21 |
+
"required_order_added": 0.3,
|
| 22 |
+
"note_grounded": 0.2,
|
| 23 |
+
"encounter_signed": 0.2
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
}
|
tsconfig.base.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "es2022"],
|
| 5 |
+
"allowJs": false,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "ESNext",
|
| 11 |
+
"moduleResolution": "Bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "preserve",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"baseUrl": "."
|
| 17 |
+
}
|
| 18 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "./tsconfig.base.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"moduleResolution": "Bundler",
|
| 6 |
+
"target": "ES2022",
|
| 7 |
+
"lib": ["ES2022"],
|
| 8 |
+
"types": ["node"]
|
| 9 |
+
},
|
| 10 |
+
"include": ["shared/**/*.ts", "prisma/**/*.ts"],
|
| 11 |
+
"exclude": ["node_modules", "apps", "env_server"]
|
| 12 |
+
}
|