From 49a7192d7eafd8ad96e8d7aafabfb68e0e533c2b Mon Sep 17 00:00:00 2001 From: Adam Kells Date: Fri, 16 Jan 2026 11:23:08 +0000 Subject: [PATCH 1/5] Redevelopment of the docs --- docs/cookbook/index.md | 166 +++++- docs/index.md | 9 +- docs/overrides/main.html | 13 + docs/overrides/welcome.html | 171 ++++++ docs/reference/concepts.md | 263 ++++++++++ docs/stylesheets/welcome.css | 571 +++++++++++++++++++++ docs/tutorials/clinicalflow/fhir-basics.md | 143 ++++++ docs/tutorials/clinicalflow/gateway.md | 183 +++++++ docs/tutorials/clinicalflow/index.md | 53 ++ docs/tutorials/clinicalflow/next-steps.md | 156 ++++++ docs/tutorials/clinicalflow/pipeline.md | 144 ++++++ docs/tutorials/clinicalflow/setup.md | 72 +++ docs/tutorials/clinicalflow/testing.md | 182 +++++++ docs/tutorials/index.md | 36 ++ mkdocs.yml | 31 +- 15 files changed, 2173 insertions(+), 20 deletions(-) create mode 100644 docs/overrides/main.html create mode 100644 docs/overrides/welcome.html create mode 100644 docs/reference/concepts.md create mode 100644 docs/stylesheets/welcome.css create mode 100644 docs/tutorials/clinicalflow/fhir-basics.md create mode 100644 docs/tutorials/clinicalflow/gateway.md create mode 100644 docs/tutorials/clinicalflow/index.md create mode 100644 docs/tutorials/clinicalflow/next-steps.md create mode 100644 docs/tutorials/clinicalflow/pipeline.md create mode 100644 docs/tutorials/clinicalflow/setup.md create mode 100644 docs/tutorials/clinicalflow/testing.md create mode 100644 docs/tutorials/index.md diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 165624b6..dd2f5629 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -1,31 +1,163 @@ -# 🍳 Cookbook: Hands-On Examples +# Cookbook -Dive into real-world, production-ready examples to learn how to build interoperable healthcare AI apps with **HealthChain**. +Hands-on, production-ready examples for building healthcare AI applications with HealthChain. ---- +
+ Filter: + HealthTech + GenAI + ML Research + Gateway + Pipeline + Interop + FHIR + CDS Hooks + Sandbox + +
-## 🚦 Getting Started +
+
-- [**Working with FHIR Sandboxes**](./setup_fhir_sandboxes.md) - *Spin up and access free Epic, Medplum, and other FHIR sandboxes for safe experimentation. This is the recommended first step before doing the detailed tutorials below.* + +
🚦
+
Working with FHIR Sandboxes
+
+ Spin up and access free Epic, Medplum, and other FHIR sandboxes for safe experimentation. Recommended first step before the other tutorials. +
+
+ FHIR + Sandbox +
+
---- + +
πŸ”¬
+
Deploy ML Models: Real-Time Alerts & Batch Screening
+
+ Deploy the same ML model two ways: CDS Hooks for point-of-care sepsis alerts, and FHIR Gateway for population-level batch screening with RiskAssessment resources. +
+
+ ML Research + Gateway + CDS Hooks +
+
+ + +
πŸ”—
+
Multi-Source Patient Data Aggregation
+
+ Merge patient data from multiple FHIR sources (Epic, Cerner, etc.), deduplicate conditions, prove provenance, and handle cross-vendor errors. Foundation for RAG and analytics workflows. +
+
+ GenAI + Gateway + FHIR +
+
+ + +
🧾
+
Automate Clinical Coding & FHIR Integration
+
+ Extract medical conditions from clinical documentation using AI, map to SNOMED CT codes, and sync as FHIR Condition resources for billing, analytics, and interoperability. +
+
+ HealthTech + Pipeline + Interop +
+
+ + +
πŸ“
+
Summarize Discharge Notes with CDS Hooks
+
+ Deploy a CDS Hooks-compliant service that listens for discharge events, auto-generates concise plain-language summaries, and delivers actionable clinical cards directly into the EHR workflow. +
+
+ HealthTech + Gateway + CDS Hooks +
+
+ + + +
+
+ + --- -!!! info "What next?" +!!! tip "What next?" See the source code for each recipe, experiment with the sandboxes, and adapt the patterns for your projects! diff --git a/docs/index.md b/docs/index.md index f3681305..da431181 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,11 @@ -# Welcome to HealthChain πŸ’« πŸ₯ +--- +template: welcome.html +hide: + - navigation + - toc +--- + +# Welcome to HealthChain HealthChain is an open-source Python toolkit that streamlines productionizing healthcare AI. Built for AI/ML practitioners, it simplifies the complexity of real-time EHR integrations by providing seamless FHIR integration, unified data pipelines, and production-ready deployment. diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 00000000..922c914f --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block footer %} + +{% endblock %} diff --git a/docs/overrides/welcome.html b/docs/overrides/welcome.html new file mode 100644 index 00000000..021305eb --- /dev/null +++ b/docs/overrides/welcome.html @@ -0,0 +1,171 @@ + + +{% extends "main.html" %} +{% block tabs %} +{{ super() }} +{% endblock %} + +{% block content %} +
+
+

Production-Ready Healthcare AI

+

+ Built-in FHIR support, real-time EHR connectivity, and deployment tooling for healthcare AI/ML systems. Skip months of custom integration work. +

+ + +
+ + +
+{% endblock %} + +{% block footer %} + +{% endblock %} diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md new file mode 100644 index 00000000..6e1cf9e4 --- /dev/null +++ b/docs/reference/concepts.md @@ -0,0 +1,263 @@ +# Core Concepts + +HealthChain has three main components that work together to connect your AI applications to healthcare systems: + +- **Gateway:** Connect to multiple healthcare systems with a single API. +- **Pipelines:** Easily build data processing pipelines for both clinical text and [FHIR](https://www.hl7.org/fhir/) data. +- **InteropEngine:** Seamlessly convert between data formats like [FHIR](https://www.hl7.org/fhir/), [HL7 CDA](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=7), and [HL7v2](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=185). + + +## Gateway + +The [**HealthChainAPI**](./gateway/api.md) provides a unified interface for connecting your AI application and models to multiple healthcare systems through a single API. It automatically handles [FHIR API](https://www.hl7.org/fhir/http.html), [CDS Hooks](https://cds-hooks.org/), and [SOAP/CDA protocols](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=7) with [OAuth2 authentication](https://oauth.net/2/). + +[(Full Documentation on Gateway)](./gateway/gateway.md) + +```python +from healthchain.gateway import HealthChainAPI, FHIRGateway +from fhir.resources.patient import Patient + +# Create your healthcare application +app = HealthChainAPI(title="My Healthcare AI App") + +# Connect to multiple FHIR servers +fhir = FHIRGateway() +fhir.add_source("epic", "fhir://fhir.epic.com/r4?client_id=...") +fhir.add_source("medplum", "fhir://api.medplum.com/fhir/R4/?client_id=...") + +# Add AI transformations to FHIR data +@fhir.transform(Patient) +def enhance_patient(id: str, source: str = None) -> Patient: + patient = fhir.read(Patient, id, source) + # Your AI logic here + patient.active = True + fhir.update(patient, source) + return patient + +# Register and run +app.register_gateway(fhir) + +# Available at: GET /fhir/transform/Patient/123?source=epic +``` + +## Pipeline + +HealthChain [**Pipelines**](./pipeline/pipeline.md) provide a flexible way to build and manage processing pipelines for NLP and ML tasks that can easily integrate with electronic health record (EHR) systems. + +You can build pipelines with three different approaches: + +### 1. Quick Inline Functions + +For quick experiments, start by picking the right [**Container**](./io/containers/containers.md) when you initialize your pipeline (e.g. `Pipeline[Document]()` for clinical text). + +Containers make your pipeline FHIR-native by loading and transforming your data (free text, EHR resources, etc.) into structured FHIR-ready formats. Just add your processing functions with `@add_node`, compile with `.build()`, and your pipeline is ready to process FHIR data end-to-end. + +[(Full Documentation on Containers)](./io/containers/containers.md) + +```python +from healthchain.pipeline import Pipeline +from healthchain.io import Document +from healthchain.fhir import create_condition + +pipeline = Pipeline[Document]() + +@pipeline.add_node +def extract_diabetes(doc: Document) -> Document: + """Adds a FHIR Condition for diabetes if mentioned in the text.""" + if "diabetes" in doc.text.lower(): + condition = create_condition( + code="73211009", + display="Diabetes mellitus", + ) + doc.fhir.problem_list.append(condition) + + return doc + +pipe = pipeline.build() + +doc = Document("Patient has a history of diabetes.") +doc = pipe(doc) + +print(doc.fhir.problem_list) # FHIR Condition +``` + +### 2. Build With Components and Adapters + +[**Components**](./pipeline/components/components.md) are reusable, stateful classes that encapsulate specific processing logic, model loading, or configuration for your pipeline. Use them to organize complex workflows, handle model state, or integrate third-party libraries with minimal setup. + +HealthChain provides a set of ready-to-use [**NLP Integrations**](./pipeline/integrations/integrations.md) for common clinical NLP and ML tasks, and you can easily implement your own. + +[(Full Documentation on Components)](./pipeline/components/components.md) + +```python +from healthchain.pipeline import Pipeline +from healthchain.pipeline.components import TextPreProcessor, SpacyNLP, TextPostProcessor +from healthchain.io import Document + +pipeline = Pipeline[Document]() + +pipeline.add_node(TextPreProcessor()) +pipeline.add_node(SpacyNLP.from_model_id("en_core_sci_sm")) +pipeline.add_node(TextPostProcessor()) + +pipe = pipeline.build() + +doc = Document("Patient presents with hypertension.") +output = pipe(doc) +``` + +You can process legacy healthcare data formats too. [**Adapters**](./io/adapters/adapters.md) convert between healthcare formats like [CDA](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=7) and your pipeline β€” just parse, process, and format without worrying about low-level data conversion. + +[(Full Documentation on Adapters)](./io/adapters/adapters.md) + +```python +from healthchain.io import CdaAdapter +from healthchain.models import CdaRequest + +# Use adapter for format conversion +adapter = CdaAdapter() +cda_request = CdaRequest(document="") + +# Parse, process, format +doc = adapter.parse(cda_request) +processed_doc = pipe(doc) +output = adapter.format(processed_doc) +``` + +### 3. Use Prebuilt Pipelines + +Prebuilt pipelines are the fastest way to jump into healthcare AI with minimal setup: just load and run. Each pipeline bundles best-practice components and models for common clinical tasks (like coding or summarization) and handles all FHIR/CDA conversion for you. Easily customize or extend pipelines by adding/removing components, or swap models as needed. + +[(Full Documentation on Pipelines)](./pipeline/pipeline.md#prebuilt-) + +```python +from healthchain.pipeline import MedicalCodingPipeline +from healthchain.models import CdaRequest + +# Or load from local model +pipeline = MedicalCodingPipeline.from_local_model("./path/to/model", source="spacy") + +cda_request = CdaRequest(document="") +output = pipeline.process_request(cda_request) +``` + +## Interoperability + +The HealthChain Interoperability module provides tools for converting between different healthcare data formats, including FHIR, CDA, and HL7v2 messages. + +[(Full Documentation on Interoperability Engine)](./interop/interop.md) + +```python +from healthchain.interop import create_interop, FormatType + +# Uses bundled configs - basic CDA ↔ FHIR conversion +engine = create_interop() + +# Load a CDA document +with open("tests/data/test_cda.xml", "r") as f: + cda_xml = f.read() + +# Convert CDA XML to FHIR resources +fhir_resources = engine.to_fhir(cda_xml, src_format=FormatType.CDA) + +# Convert FHIR resources back to CDA +cda_document = engine.from_fhir(fhir_resources, dest_format=FormatType.CDA) +``` + + +## Utilities + +### Sandbox Client + +Use [**SandboxClient**](./utilities/sandbox.md) to quickly test your app against real-world EHR scenarios like CDS Hooks or Clinical Documentation Improvement (CDI) workflows. Load test datasets, send requests to your service, and validate responses in a few lines of code. + +[(Full Documentation on Sandbox)](./utilities/sandbox.md) + +#### Workflows + +A [**workflow**](./utilities/sandbox.md#workflow-protocol-compatibility) represents a specific event in an EHR system that triggers your service (e.g., `patient-view` when opening a patient chart, `encounter-discharge` when discharging a patient). + +Workflows determine the request structure, required FHIR resources, and validation rules. Different workflows are compatible with different protocols: + +| Workflow Type | Protocol | Example Workflows | +|-------------------------------------|------------|--------------------------------------------------------| +| **CDS Hooks** | REST | `patient-view`, `order-select`, `order-sign`, `encounter-discharge` | +| **Clinical Documentation** | SOAP | `sign-note-inpatient`, `sign-note-outpatient` | + + +#### Available Dataset Loaders + +[**Dataset Loaders**](./utilities/sandbox.md#dataset-loaders) are shortcuts for loading common clinical test datasets from file. Currently available: + +| Dataset Key | Description | FHIR Version | Source | Download Link | +|--------------------|---------------------------------------------|--------------|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| `mimic-on-fhir` | **MIMIC-IV on FHIR Demo Dataset** | R4 | [PhysioNet Project](https://physionet.org/content/mimic-iv-fhir-demo/2.1.0/) | [Download ZIP](https://physionet.org/content/mimic-iv-fhir-demo/get-zip/2.1.0/) (49.5 MB) | +| `synthea-patient` | **Synthea FHIR Patient Records** | R4 | [Synthea Downloads](https://synthea.mitre.org/downloads) | [Download ZIP](https://arc.net/l/quote/hoquexhy) (100 Sample, 36 MB) | + + +```python +from healthchain.sandbox import list_available_datasets + +# See all registered datasets with descriptions +datasets = list_available_datasets() +print(datasets) +``` + +#### Basic Usage + +```python +from healthchain.sandbox import SandboxClient + +# Initialize client with your service URL and workflow +client = SandboxClient( + url="http://localhost:8000/cds/encounter-discharge", + workflow="encounter-discharge" +) + +# Load test data from a registered dataset +client.load_from_registry( + "synthea-patient", + data_dir="./data/synthea", + resource_types=["Condition", "DocumentReference"], + sample_size=3 +) + +# Optionally inspect before sending +client.preview_requests() # See what will be sent +client.get_status() # Check client state + +# Send requests to your service +responses = client.send_requests() +``` + +For clinical documentation workflows using SOAP/CDA: + +```python +# Use context manager for automatic result saving +with SandboxClient( + url="http://localhost:8000/notereader/ProcessDocument", + workflow="sign-note-inpatient", + protocol="soap" +) as client: + client.load_from_path("./cookbook/data/notereader_cda.xml") + responses = client.send_requests() + # Results automatically saved to ./output/ on success +``` + +### FHIR Helpers + +Use `healthchain.fhir` helpers to quickly create and manipulate FHIR resources (like `Condition`, `Observation`, etc.) in your code, ensuring they're standards-compliant with minimal boilerplate. + +[(Full Documentation on FHIR Helpers)](./utilities/fhir_helpers.md) + +```python +from healthchain.fhir import create_condition + +condition = create_condition( + code="38341003", + display="Hypertension", + system="http://snomed.info/sct", + subject="Patient/Foo", + clinical_status="active" +) +``` diff --git a/docs/stylesheets/welcome.css b/docs/stylesheets/welcome.css new file mode 100644 index 00000000..4debf984 --- /dev/null +++ b/docs/stylesheets/welcome.css @@ -0,0 +1,571 @@ +/* HealthChain Welcome Page Styles + Adapted from Kedro's documentation pattern */ + +/* CSS Variables for theming */ +:root { + --welcome-page-bg-color: #ffffff; + --cards-section-bg-color: #f8f9fa; + --card-bg-color: #ffffff; + --card-border-color: #e0e0e0; + --card-featured-border-color: #e59875; + --card-featured-bg-color: #fef7f4; + --text-primary-color: #1a1a1a; + --text-secondary-color: #666666; + --accent-color: #e59875; + --accent-secondary: #79a8a9; +} + +[data-md-color-scheme="slate"] { + --welcome-page-bg-color: #1e1e1e; + --cards-section-bg-color: #2d2d2d; + --card-bg-color: #2d2d2d; + --card-border-color: #404040; + --card-featured-border-color: #e59875; + --card-featured-bg-color: #3d2d28; + --text-primary-color: #ffffff; + --text-secondary-color: #b0b0b0; +} + +/* Target welcome page container specifically */ +.md-content__inner:has(.welcome-page-container) { + padding: 0 !important; + margin: 0 !important; + max-width: none !important; + background-color: var(--welcome-page-bg-color); +} + +/* Main Container */ +.welcome-page-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +/* Hero Section */ +.welcome-page-container header { + text-align: center; + padding: 0 16px; +} + +.welcome-title { + margin-top: 80px !important; + margin-bottom: 16px !important; + font-weight: 700 !important; + font-size: 2.5rem !important; + color: var(--text-primary-color); + line-height: 1.2; +} + +.welcome-subtitle { + font-size: 1.125rem; + color: var(--text-secondary-color); + max-width: 600px; + margin: 0 auto 24px auto; + line-height: 1.6; +} + +/* Explore/CTA Link */ +.explore-link { + text-align: center; + margin: 32px 0 64px 0; +} + +.explore-link a { + display: inline-flex; + align-items: center; + padding: 12px 24px; + background-color: var(--accent-color); + color: #ffffff !important; + text-decoration: none !important; + border-radius: 8px; + font-weight: 600; + font-size: 1rem; + transition: background-color 0.2s, transform 0.2s; +} + +.explore-link a:hover { + background-color: #d4845f; + transform: translateY(-2px); +} + +.explore-link .arrow { + margin-left: 8px; + font-size: 1.2rem; +} + +/* Cards Section Wrapper */ +.cards-section-wrapper { + background-color: var(--cards-section-bg-color); + width: 100%; + padding: 48px 0 64px 0; +} + +.cards-section-wrapper section { + margin-bottom: 48px; + padding: 0 16px; +} + +.cards-section-wrapper section:last-child { + margin-bottom: 0; +} + +/* Section Titles */ +.section-title { + font-weight: 600; + font-size: 1.25rem; + text-align: center; + margin-bottom: 24px !important; + margin-top: 0; + color: var(--text-primary-color); +} + +/* Card Grid */ +.card-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin: 0 auto; + max-width: 900px; +} + +.card-grid.two-col { + grid-template-columns: repeat(2, 1fr); + max-width: 600px; +} + +/* Card Styles */ +.card { + background-color: var(--card-bg-color); + border: 2px solid var(--card-border-color); + border-radius: 16px; + padding: 24px; + transition: transform 0.2s, box-shadow 0.2s; + text-decoration: none !important; + display: flex; + flex-direction: column; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.1); +} + +/* Featured Card (Primary Persona) */ +.card.card-featured { + border-color: var(--card-featured-border-color); + background-color: var(--card-featured-bg-color); + position: relative; +} + +.card.card-featured::before { + content: "Recommended"; + position: absolute; + top: -12px; + left: 16px; + background-color: var(--accent-color); + color: #ffffff; + font-size: 0.75rem; + font-weight: 600; + padding: 4px 12px; + border-radius: 12px; +} + +/* Card Icon */ +.card-icon { + margin-bottom: 16px; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--cards-section-bg-color); + border-radius: 12px; +} + +.card-icon svg { + width: 28px; + height: 28px; +} + +.card-icon svg path { + fill: var(--accent-color); +} + +/* Card Content */ +.card-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.card-title { + font-size: 1rem !important; + font-weight: 600 !important; + color: var(--text-primary-color); + margin-bottom: 8px !important; + margin-top: 0 !important; +} + +.card-description { + font-size: 0.875rem; + color: var(--text-secondary-color); + line-height: 1.5; + margin: 0; + flex: 1; +} + +.card-cta { + margin-top: 16px; + font-size: 0.875rem; + font-weight: 600; + color: var(--accent-color); +} + +/* Welcome Page Footer */ +.welcome-page-footer { + background-color: var(--welcome-page-bg-color); + padding: 32px 16px; + text-align: center; +} + +.welcome-page-footer .badges { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} + +.welcome-page-footer .badges img { + height: 20px; +} + +/* Mobile Responsive */ +@media screen and (max-width: 76.2344em) { + .card-grid, + .card-grid.two-col { + grid-template-columns: 1fr; + gap: 20px; + max-width: 100%; + padding: 0 8px; + } + + .cards-section-wrapper { + padding: 32px 0 48px 0; + } + + .cards-section-wrapper section { + padding: 0 8px; + } + + .card { + padding: 20px; + } + + .welcome-title { + margin-top: 48px !important; + font-size: 1.75rem !important; + } + + .welcome-subtitle { + font-size: 1rem; + } + + .explore-link { + margin: 24px 0 48px 0; + } + + .section-title { + margin-top: 24px; + margin-bottom: 20px !important; + } + + .card.card-featured::before { + top: -10px; + font-size: 0.7rem; + padding: 3px 10px; + } +} + +/* Tablet breakpoint */ +@media screen and (min-width: 48em) and (max-width: 76.2344em) { + .card-grid { + grid-template-columns: repeat(2, 1fr); + } + + .card-grid.two-col { + grid-template-columns: repeat(2, 1fr); + } +} + +/* ============================================ + Cookbook Cards and Tags + ============================================ */ + +/* Cookbook wrapper with grey background */ +.cookbook-wrapper { + background-color: var(--cards-section-bg-color); + margin: 0 -0.8rem 1rem -0.8rem; + padding: 32px 24px; + border-radius: 12px; +} + +/* Cookbook card grid - 2 columns for better readability */ +.cookbook-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + margin: 0; +} + +@media screen and (max-width: 76.2344em) { + .cookbook-grid { + grid-template-columns: 1fr; + } +} + +/* Cookbook card */ +.cookbook-card { + background-color: var(--md-default-bg-color); + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 12px; + padding: 24px; + transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; + text-decoration: none !important; + display: flex; + flex-direction: column; + height: 100%; +} + +.cookbook-card:hover { + transform: translateY(-3px); + box-shadow: 0 6px 16px -4px rgba(0, 0, 0, 0.1); + border-color: var(--md-accent-fg-color); +} + +.cookbook-card-icon { + font-size: 2rem; + margin-bottom: 12px; +} + +.cookbook-card-title { + font-size: 1.1rem !important; + font-weight: 600 !important; + color: var(--md-default-fg-color); + margin: 0 0 8px 0 !important; + line-height: 1.3; +} + +.cookbook-card-description { + font-size: 0.9rem; + color: var(--md-default-fg-color--light); + line-height: 1.5; + margin: 0 0 16px 0; + flex: 1; +} + +/* Tag container */ +.cookbook-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: auto; +} + +/* Base tag style */ +.tag { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +/* Persona tags */ +.tag-healthtech { + background-color: #fef3e7; + color: #c76a15; +} + +.tag-genai { + background-color: #e8f4f8; + color: #1976a2; +} + +.tag-ml { + background-color: #f3e8f8; + color: #7b1fa2; +} + +/* Feature tags */ +.tag-gateway { + background-color: #e8f5e9; + color: #2e7d32; +} + +.tag-pipeline { + background-color: #fff3e0; + color: #e65100; +} + +.tag-interop { + background-color: #e3f2fd; + color: #1565c0; +} + +.tag-sandbox { + background-color: #fce4ec; + color: #c2185b; +} + +.tag-fhir { + background-color: #f1f8e9; + color: #558b2f; +} + +.tag-cdshooks { + background-color: #fff8e1; + color: #ff8f00; +} + +/* Dark mode tag adjustments */ +[data-md-color-scheme="slate"] .tag-healthtech { + background-color: #3d2d1f; + color: #ffb74d; +} + +[data-md-color-scheme="slate"] .tag-genai { + background-color: #1a3a4a; + color: #4fc3f7; +} + +[data-md-color-scheme="slate"] .tag-ml { + background-color: #2d1f3d; + color: #ce93d8; +} + +[data-md-color-scheme="slate"] .tag-gateway { + background-color: #1b3d1f; + color: #81c784; +} + +[data-md-color-scheme="slate"] .tag-pipeline { + background-color: #3d2a1a; + color: #ffb74d; +} + +[data-md-color-scheme="slate"] .tag-interop { + background-color: #1a2d3d; + color: #64b5f6; +} + +[data-md-color-scheme="slate"] .tag-sandbox { + background-color: #3d1a2a; + color: #f48fb1; +} + +[data-md-color-scheme="slate"] .tag-fhir { + background-color: #2a3d1a; + color: #aed581; +} + +[data-md-color-scheme="slate"] .tag-cdshooks { + background-color: #3d3a1a; + color: #ffd54f; +} + +[data-md-color-scheme="slate"] .cookbook-card { + background-color: var(--md-default-bg-color); + border-color: var(--md-default-fg-color--lightest); +} + +/* Tag legend / filter bar */ +.tag-legend { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 24px; + padding: 16px 20px; + background-color: var(--md-default-bg-color); + border-radius: 8px; + border: 1px solid var(--md-default-fg-color--lightest); +} + +.tag-legend-title { + font-size: 0.8rem; + font-weight: 600; + color: var(--md-default-fg-color--light); + margin-right: 8px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Clickable filter tags */ +.tag-filter { + cursor: pointer; + transition: transform 0.15s, opacity 0.15s, box-shadow 0.15s; + user-select: none; + opacity: 0.6; +} + +.tag-filter:hover { + transform: scale(1.05); + opacity: 0.85; +} + +.tag-filter.active { + opacity: 1; + box-shadow: 0 0 0 2px var(--md-default-bg-color), 0 0 0 4px currentColor; +} + +/* Clear filter button */ +.tag-clear { + cursor: pointer; + padding: 3px 10px; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 600; + background-color: var(--md-default-fg-color--lightest); + color: var(--md-default-fg-color--light); + transition: background-color 0.15s; + margin-left: auto; +} + +.tag-clear:hover { + background-color: var(--md-default-fg-color--lighter); +} + +.tag-clear.hidden { + display: none; +} + +/* Card filtering states */ +.cookbook-card.filtered-out { + display: none; +} + +.cookbook-card.filtered-in { + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* No results message */ +.no-results { + grid-column: 1 / -1; + text-align: center; + padding: 48px 24px; + color: var(--md-default-fg-color--light); + font-size: 1rem; +} + +.no-results.hidden { + display: none; +} diff --git a/docs/tutorials/clinicalflow/fhir-basics.md b/docs/tutorials/clinicalflow/fhir-basics.md new file mode 100644 index 00000000..30f93446 --- /dev/null +++ b/docs/tutorials/clinicalflow/fhir-basics.md @@ -0,0 +1,143 @@ +# FHIR Basics + +Understand the healthcare data format you'll work with in HealthChain. + +## What is FHIR? + +**FHIR** (Fast Healthcare Interoperability Resources) is the modern standard for exchanging healthcare data. Think of it as "JSON for healthcare" - structured, standardized data that EHR systems like Epic and Cerner use. + +## Key Resources for CDS + +For our ClinicalFlow service, we'll work with three main FHIR resources: + +### Patient + +Identifies who the patient is: + +```json +{ + "resourceType": "Patient", + "id": "example-patient", + "name": [{"given": ["John"], "family": "Smith"}], + "birthDate": "1970-01-15", + "gender": "male" +} +``` + +### Condition + +Records diagnoses and health problems: + +```json +{ + "resourceType": "Condition", + "id": "example-condition", + "code": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "38341003", + "display": "Hypertension" + }] + }, + "subject": {"reference": "Patient/example-patient"}, + "clinicalStatus": { + "coding": [{"code": "active"}] + } +} +``` + +### MedicationStatement + +Tracks what medications a patient is taking: + +```json +{ + "resourceType": "MedicationStatement", + "id": "example-med", + "medicationCodeableConcept": { + "coding": [{ + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "197361", + "display": "Lisinopril 10 MG" + }] + }, + "subject": {"reference": "Patient/example-patient"}, + "status": "active" +} +``` + +## Working with FHIR in HealthChain + +HealthChain provides utilities to work with FHIR resources easily: + +```python +from healthchain.fhir import create_condition, create_patient +from fhir.resources.bundle import Bundle + +# Create a patient +patient = create_patient( + id="patient-001", + given_name="John", + family_name="Smith", + birth_date="1970-01-15" +) + +# Create a condition +condition = create_condition( + id="condition-001", + code="38341003", + display="Hypertension", + system="http://snomed.info/sct", + patient_reference="Patient/patient-001" +) + +print(f"Created patient: {patient.name[0].given[0]} {patient.name[0].family}") +print(f"With condition: {condition.code.coding[0].display}") +``` + +## FHIR Bundles + +When an EHR sends patient context, it often comes as a **Bundle** - a collection of related resources: + +```python +from fhir.resources.bundle import Bundle + +# A bundle might contain a patient, their conditions, and medications +bundle_data = { + "resourceType": "Bundle", + "type": "collection", + "entry": [ + {"resource": patient.dict()}, + {"resource": condition.dict()} + ] +} + +bundle = Bundle(**bundle_data) +print(f"Bundle contains {len(bundle.entry)} resources") +``` + +## The Document Container + +HealthChain's `Document` container bridges clinical text and FHIR data: + +```python +from healthchain.io import Document + +# Create a document with clinical text +doc = Document( + "Patient presents with chest pain and shortness of breath. " + "History of hypertension and diabetes." +) + +# The document can hold FHIR data extracted from text +print(f"Document text: {doc.text[:50]}...") + +# After NLP processing, the document will contain: +# - Extracted entities +# - Generated FHIR resources +# - Problem lists, medications, etc. +``` + +## What's Next + +Now that you understand FHIR basics, let's [build a pipeline](pipeline.md) that processes clinical text and extracts structured data. diff --git a/docs/tutorials/clinicalflow/gateway.md b/docs/tutorials/clinicalflow/gateway.md new file mode 100644 index 00000000..1469ff8f --- /dev/null +++ b/docs/tutorials/clinicalflow/gateway.md @@ -0,0 +1,183 @@ +# Create Gateway + +Expose your pipeline as a CDS Hooks service that EHRs can call. + +## What is CDS Hooks? + +**CDS Hooks** is a standard for integrating clinical decision support with EHR systems. When a clinician performs an action (like opening a patient chart), the EHR calls your CDS service, and you return helpful information as "cards." + +The flow: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Clinician │────────>β”‚ EHR │────────>β”‚ Your CDS β”‚ +β”‚ opens β”‚ β”‚ (Epic, β”‚ HTTP β”‚ Service β”‚ +β”‚ chart β”‚ β”‚ Cerner) β”‚ POST β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚<──────────────────────│ + β”‚ CDS Cards β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Display β”‚ + β”‚ alerts to β”‚ + β”‚ clinician β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Create the CDS Service + +Create a file called `app.py`: + +```python +from healthchain.gateway import HealthChainAPI, CDSHooksService +from healthchain.io import Document +from pipeline import create_clinical_pipeline + +# Initialize the HealthChain API +app = HealthChainAPI(title="ClinicalFlow CDS Service") + +# Create your pipeline +nlp = create_clinical_pipeline() + +# Define the CDS Hooks service +@app.cds_hooks( + id="patient-alerts", + title="Clinical Alert Service", + description="Analyzes patient data and returns relevant clinical alerts", + hook="patient-view" # Triggers when a clinician views a patient +) +def patient_alerts(context, prefetch): + """ + Process patient context and return CDS cards. + + Args: + context: CDS Hooks context (patient ID, user, etc.) + prefetch: Pre-fetched FHIR resources + """ + cards = [] + + # Get patient conditions from prefetch (if available) + conditions = prefetch.get("conditions", []) + + # If we have clinical notes, process them + if clinical_note := prefetch.get("note"): + doc = Document(clinical_note) + result = nlp(doc) + + # Create cards for each extracted condition + for entity in result.entities: + cards.append({ + "summary": f"Condition detected: {entity['display']}", + "detail": f"SNOMED code: {entity['code']}", + "indicator": "info", + "source": { + "label": "ClinicalFlow", + "url": "https://healthchain.dev" + } + }) + + # Check for drug interaction alerts + if len(conditions) > 2: + cards.append({ + "summary": "Multiple active conditions", + "detail": f"Patient has {len(conditions)} active conditions. Review for potential interactions.", + "indicator": "warning", + "source": {"label": "ClinicalFlow"} + }) + + return cards + + +# Run the server +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +## Understanding the Code + +### The `@app.cds_hooks` Decorator + +This decorator registers your function as a CDS Hooks endpoint: + +- **`id`**: Unique identifier for this service +- **`title`**: Human-readable name +- **`hook`**: When to trigger (e.g., `patient-view`, `order-select`) + +### CDS Cards + +Cards are the responses you return to the EHR. Each card has: + +| Field | Description | +|-------|-------------| +| `summary` | Brief message shown to clinician | +| `detail` | Additional information (optional) | +| `indicator` | Urgency: `info`, `warning`, or `critical` | +| `source` | Attribution for the recommendation | + +## Run the Service + +Start your CDS service: + +```bash +python app.py +``` + +Your service is now running at `http://localhost:8000`. + +## Test the Endpoints + +### Discovery Endpoint + +CDS Hooks services must provide a discovery endpoint. Test it: + +```bash +curl http://localhost:8000/cds-services +``` + +Response: + +```json +{ + "services": [ + { + "id": "patient-alerts", + "title": "Clinical Alert Service", + "description": "Analyzes patient data and returns relevant clinical alerts", + "hook": "patient-view" + } + ] +} +``` + +### Service Endpoint + +Test calling your service: + +```bash +curl -X POST http://localhost:8000/cds-services/patient-alerts \ + -H "Content-Type: application/json" \ + -d '{ + "hookInstance": "test-123", + "hook": "patient-view", + "context": { + "patientId": "patient-001", + "userId": "doctor-001" + }, + "prefetch": { + "note": "Patient presents with chest pain and hypertension." + } + }' +``` + +## Interactive API Docs + +HealthChain generates OpenAPI documentation. Visit: + +- **Swagger UI**: `http://localhost:8000/docs` +- **ReDoc**: `http://localhost:8000/redoc` + +## What's Next + +Your CDS service is running! Now let's [test it properly](testing.md) with realistic patient data using HealthChain's sandbox. diff --git a/docs/tutorials/clinicalflow/index.md b/docs/tutorials/clinicalflow/index.md new file mode 100644 index 00000000..e4a3da54 --- /dev/null +++ b/docs/tutorials/clinicalflow/index.md @@ -0,0 +1,53 @@ +# ClinicalFlow Tutorial + +Build your first Clinical Decision Support (CDS) service with HealthChain. + +## The Scenario + +You're a HealthTech engineer at a hospital system. The clinical informatics team needs a CDS service that: + +1. **Receives patient context** when a physician opens a chart +2. **Analyzes existing conditions and medications** +3. **Returns actionable alerts** for potential drug interactions or care gaps + +By the end of this tutorial, you'll have a working CDS Hooks service that integrates with EHR systems like Epic. + +## What You'll Build + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ EHR System │─────>β”‚ Your CDS │─────>β”‚ Clinical β”‚ +β”‚ (Epic, etc.) β”‚ β”‚ Service β”‚ β”‚ Alert Cards β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚ β”‚ + β–Ό β–Ό + Patient context NLP Pipeline + (FHIR resources) (HealthChain) +``` + +## What You'll Learn + +| Step | What You'll Learn | +|------|-------------------| +| [Setup](setup.md) | Install dependencies, create project structure | +| [FHIR Basics](fhir-basics.md) | Understand Patient, Condition, and Medication resources | +| [Build Pipeline](pipeline.md) | Create an NLP pipeline with Document containers | +| [Create Gateway](gateway.md) | Expose your pipeline as a CDS Hooks service | +| [Test with Sandbox](testing.md) | Validate with synthetic patient data | +| [Next Steps](next-steps.md) | Production deployment and extending your service | + +## Prerequisites + +- **Python 3.10+** installed +- **Basic Python knowledge** (functions, classes, imports) +- **REST API familiarity** (HTTP methods, JSON) +- Healthcare knowledge is helpful but not required + +## Time Required + +This tutorial takes approximately **45 minutes** to complete. + +## Ready? + +Let's start by [setting up your project](setup.md). diff --git a/docs/tutorials/clinicalflow/next-steps.md b/docs/tutorials/clinicalflow/next-steps.md new file mode 100644 index 00000000..53616e05 --- /dev/null +++ b/docs/tutorials/clinicalflow/next-steps.md @@ -0,0 +1,156 @@ +# Next Steps + +You've built a working CDS service. Here's how to take it further. + +## What You've Accomplished + +In this tutorial, you: + +- Set up a HealthChain development environment +- Learned FHIR basics (Patient, Condition, MedicationStatement) +- Built an NLP pipeline with Document containers +- Created a CDS Hooks gateway service +- Tested with the sandbox and synthetic data + +## Production Considerations + +### Authentication + +Real EHR integrations require OAuth2 authentication: + +```python +from healthchain.gateway import HealthChainAPI + +app = HealthChainAPI( + title="ClinicalFlow CDS Service", + auth_config={ + "type": "oauth2", + "token_url": "https://your-auth-server/token", + "scopes": ["patient/*.read", "user/*.read"] + } +) +``` + +### HTTPS + +Always use HTTPS in production. With uvicorn: + +```bash +uvicorn app:app --host 0.0.0.0 --port 443 --ssl-keyfile key.pem --ssl-certfile cert.pem +``` + +### Logging and Monitoring + +Add structured logging: + +```python +import logging + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger("clinicalflow") + +@app.cds_hooks(id="patient-alerts", ...) +def patient_alerts(context, prefetch): + logger.info(f"Processing request for patient {context.get('patientId')}") + # ... your logic +``` + +## Connect to Real EHR Sandboxes + +### Epic Sandbox + +1. Register at [Epic's Developer Portal](https://fhir.epic.com/) +2. Create an application +3. Configure your service URL +4. Test against Epic's sandbox environment + +### Cerner Sandbox + +1. Register at [Cerner's Developer Portal](https://code.cerner.com/) +2. Follow their CDS Hooks integration guide +3. Test with their Millennium sandbox + +## Extend Your Service + +### Add More Hooks + +Support multiple trigger points: + +```python +@app.cds_hooks( + id="order-check", + title="Medication Order Check", + hook="order-select" +) +def check_medication_orders(context, prefetch): + """Check for drug interactions when orders are selected.""" + # ... drug interaction logic + pass + + +@app.cds_hooks( + id="discharge-summary", + title="Discharge Summary Generator", + hook="encounter-discharge" +) +def generate_discharge_summary(context, prefetch): + """Generate discharge summary at end of encounter.""" + # ... summarization logic + pass +``` + +### Improve NLP + +Replace keyword matching with trained models: + +```python +from healthchain.pipeline.components.integrations import SpacyNLP + +# Use a clinical NLP model +pipeline.add_node(SpacyNLP.from_model_id("en_core_sci_lg")) + +# Or integrate with external services +from healthchain.pipeline.components import LLMComponent + +pipeline.add_node(LLMComponent( + provider="openai", + model="gpt-4", + prompt_template="Extract clinical conditions from: {text}" +)) +``` + +### Add FHIR Output + +Convert extracted entities to FHIR resources: + +```python +from healthchain.pipeline.components import FHIRProblemListExtractor + +pipeline.add_node(FHIRProblemListExtractor()) + +# Now doc.fhir.problem_list contains FHIR Condition resources +``` + +## Learn More + +Explore HealthChain's documentation: + +| Topic | Description | +|-------|-------------| +| [Gateway Reference](../../reference/gateway/gateway.md) | Deep dive into gateway patterns | +| [Pipeline Reference](../../reference/pipeline/pipeline.md) | Advanced pipeline configuration | +| [CDS Hooks Cookbook](../../cookbook/discharge_summarizer.md) | Complete CDS Hooks example | +| [Multi-EHR Integration](../../cookbook/multi_ehr_aggregation.md) | Connect to multiple EHRs | + +## Get Help + +- **Discord**: [Join our community](https://discord.gg/UQC6uAepUz) +- **GitHub**: [Report issues](https://github.com/dotimplement/healthchain/issues) +- **Office Hours**: Thursdays 4:30-5:30pm GMT + +## Congratulations! + +You've completed the ClinicalFlow tutorial. You now have the foundation to build production-ready healthcare AI applications with HealthChain. diff --git a/docs/tutorials/clinicalflow/pipeline.md b/docs/tutorials/clinicalflow/pipeline.md new file mode 100644 index 00000000..9e06255c --- /dev/null +++ b/docs/tutorials/clinicalflow/pipeline.md @@ -0,0 +1,144 @@ +# Build Pipeline + +Create an NLP pipeline to process clinical text and extract structured data. + +## What is a Pipeline? + +A **Pipeline** in HealthChain is a sequence of processing steps that transform input data. For clinical NLP, pipelines typically: + +1. Take clinical text as input +2. Process it through NLP models +3. Extract entities (conditions, medications, etc.) +4. Output structured FHIR resources + +## Create Your Pipeline + +Create a file called `pipeline.py`: + +```python +from healthchain.pipeline import Pipeline +from healthchain.io import Document + +def create_clinical_pipeline(): + """Create a pipeline for processing clinical notes.""" + + # Initialize pipeline with Document as the data container + pipeline = Pipeline[Document]() + + # Add a simple text preprocessing component + @pipeline.add_node + def preprocess(doc: Document) -> Document: + """Clean and normalize clinical text.""" + # Remove extra whitespace + doc.text = " ".join(doc.text.split()) + return doc + + # Add a clinical entity extraction component + @pipeline.add_node + def extract_conditions(doc: Document) -> Document: + """Extract condition mentions from text.""" + # Simple keyword-based extraction for this tutorial + # In production, you'd use a trained NLP model + condition_keywords = { + "hypertension": ("38341003", "Hypertension"), + "diabetes": ("73211009", "Diabetes mellitus"), + "chest pain": ("29857009", "Chest pain"), + "shortness of breath": ("267036007", "Dyspnea"), + } + + text_lower = doc.text.lower() + extracted = [] + + for keyword, (code, display) in condition_keywords.items(): + if keyword in text_lower: + extracted.append({ + "text": keyword, + "code": code, + "display": display, + "system": "http://snomed.info/sct" + }) + + # Store extracted conditions in document + doc.entities = extracted + return doc + + return pipeline.build() +``` + +## Using the Pipeline + +Test your pipeline: + +```python +from pipeline import create_clinical_pipeline +from healthchain.io import Document + +# Create the pipeline +nlp = create_clinical_pipeline() + +# Process a clinical note +doc = Document( + "Patient is a 65-year-old male presenting with chest pain " + "and shortness of breath. History includes hypertension " + "and diabetes, both well-controlled on current medications." +) + +# Run the pipeline +result = nlp(doc) + +# Check extracted entities +print("Extracted conditions:") +for entity in result.entities: + print(f" - {entity['display']} (SNOMED: {entity['code']})") +``` + +Expected output: + +``` +Extracted conditions: + - Hypertension (SNOMED: 38341003) + - Diabetes mellitus (SNOMED: 73211009) + - Chest pain (SNOMED: 29857009) + - Dyspnea (SNOMED: 267036007) +``` + +## Adding SpaCy Integration (Optional) + +For more sophisticated NLP, integrate spaCy: + +```python +from healthchain.pipeline import Pipeline +from healthchain.pipeline.components.integrations import SpacyNLP +from healthchain.io import Document + +def create_spacy_pipeline(): + """Create a pipeline with spaCy NLP.""" + + pipeline = Pipeline[Document]() + + # Add spaCy for tokenization and NER + pipeline.add_node(SpacyNLP.from_model_id("en_core_web_sm")) + + return pipeline.build() +``` + +## Pipeline Architecture + +Your pipeline now follows this flow: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Clinical │────>β”‚ Preprocess │────>β”‚ Extract β”‚ +β”‚ Text β”‚ β”‚ Text β”‚ β”‚ Conditions β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Document β”‚ + β”‚ + Entities β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## What's Next + +Now that you have a working pipeline, let's [create a Gateway](gateway.md) to expose it as a CDS Hooks service. diff --git a/docs/tutorials/clinicalflow/setup.md b/docs/tutorials/clinicalflow/setup.md new file mode 100644 index 00000000..92b1f560 --- /dev/null +++ b/docs/tutorials/clinicalflow/setup.md @@ -0,0 +1,72 @@ +# Setup + +Get your development environment ready for building the ClinicalFlow service. + +## Install HealthChain + +Create a new project directory and install HealthChain: + +```bash +mkdir clinicalflow +cd clinicalflow +pip install healthchain +``` + +For NLP capabilities, install with the optional spaCy integration: + +```bash +pip install healthchain[nlp] +``` + +## Verify Installation + +Create a file called `check_install.py`: + +```python +import healthchain +from healthchain.io import Document + +print(f"HealthChain version: {healthchain.__version__}") + +# Test creating a simple document +doc = Document("Patient has a history of hypertension.") +print(f"Created document with {len(doc.text)} characters") +``` + +Run it: + +```bash +python check_install.py +``` + +You should see output like: + +``` +HealthChain version: 0.x.x +Created document with 40 characters +``` + +## Project Structure + +Create the following project structure: + +``` +clinicalflow/ +β”œβ”€β”€ app.py # Main CDS Hooks service +β”œβ”€β”€ pipeline.py # NLP processing pipeline +└── test_service.py # Testing script +``` + +## Download Sample Data (Optional) + +For testing, you can use Synthea-generated patient data. HealthChain's sandbox can load this automatically, but if you want local data: + +```bash +mkdir data +# Download a sample Synthea bundle (optional) +# We'll use HealthChain's built-in data loaders in the testing step +``` + +## What's Next + +Now that your environment is set up, let's learn about [FHIR basics](fhir-basics.md) - the healthcare data format you'll be working with. diff --git a/docs/tutorials/clinicalflow/testing.md b/docs/tutorials/clinicalflow/testing.md new file mode 100644 index 00000000..1f0c2eab --- /dev/null +++ b/docs/tutorials/clinicalflow/testing.md @@ -0,0 +1,182 @@ +# Test with Sandbox + +Validate your CDS service with realistic patient data using HealthChain's sandbox. + +## What is the Sandbox? + +The **Sandbox** provides tools for testing CDS services without connecting to a real EHR. It can: + +- Generate realistic patient data +- Send CDS Hooks requests to your service +- Validate responses against the specification +- Save results for analysis + +## Create a Test Script + +Create a file called `test_service.py`: + +```python +from healthchain.sandbox import SandboxClient + +# Create a sandbox client pointing to your service +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) + +# Generate synthetic test data +client.generate_data( + num_patients=3, + conditions_per_patient=2 +) + +# Send requests and collect responses +responses = client.send_requests() + +# Analyze results +print(f"Sent {len(responses)} requests") +for i, response in enumerate(responses): + print(f"\nPatient {i + 1}:") + print(f" Status: {response.status_code}") + if response.ok: + cards = response.json().get("cards", []) + print(f" Cards returned: {len(cards)}") + for card in cards: + print(f" - {card.get('indicator', 'info').upper()}: {card.get('summary')}") +``` + +## Run the Test + +Make sure your service is running, then: + +```bash +python test_service.py +``` + +Expected output: + +``` +Sent 3 requests + +Patient 1: + Status: 200 + Cards returned: 2 + - INFO: Condition detected: Hypertension + - WARNING: Multiple active conditions + +Patient 2: + Status: 200 + Cards returned: 1 + - INFO: Condition detected: Diabetes mellitus + +Patient 3: + Status: 200 + Cards returned: 3 + - INFO: Condition detected: Chest pain + - INFO: Condition detected: Hypertension + - WARNING: Multiple active conditions +``` + +## Using Real Test Datasets + +Load data from Synthea (a synthetic patient generator): + +```python +from healthchain.sandbox import SandboxClient + +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) + +# Load from Synthea data directory +client.load_from_registry( + "synthea-patient", + data_dir="./data/synthea", + resource_types=["Patient", "Condition", "MedicationStatement"], + sample_size=5 +) + +responses = client.send_requests() +``` + +## Save Test Results + +Save results for reporting or debugging: + +```python +# Save responses to files +client.save_results("./output/test_results/") + +# Results are saved as JSON: +# - output/test_results/request_1.json +# - output/test_results/response_1.json +# - output/test_results/summary.json +``` + +## Validate CDS Hooks Compliance + +The sandbox validates that responses meet the CDS Hooks specification: + +```python +from healthchain.sandbox import SandboxClient + +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) + +# Enable strict validation +client.validate_responses = True + +responses = client.send_requests() + +# Check for validation errors +for response in responses: + if response.validation_errors: + print(f"Validation errors: {response.validation_errors}") +``` + +## Testing Different Hooks + +Test different CDS Hooks workflows: + +```python +# Test order-select hook +order_client = SandboxClient( + url="http://localhost:8000/cds-services/drug-interactions", + workflow="order-select" +) + +# Test order-sign hook +sign_client = SandboxClient( + url="http://localhost:8000/cds-services/order-review", + workflow="order-sign" +) +``` + +## Debugging Tips + +### Enable Verbose Logging + +```python +import logging +logging.basicConfig(level=logging.DEBUG) + +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) +``` + +### Inspect Request/Response + +```python +response = client.send_single_request(patient_data) +print("Request sent:", response.request_body) +print("Response received:", response.json()) +``` + +## What's Next + +Your service is tested and working! Learn about [production deployment](next-steps.md) and extending your CDS service. diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 00000000..8e3f0f76 --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,36 @@ +# Tutorials + +Learn HealthChain through hands-on, step-by-step tutorials. Each tutorial is designed to teach core concepts while building something practical. + +## Available Tutorials + +### ClinicalFlow Tutorial + +**Build a Clinical Decision Support Service** + +The ClinicalFlow tutorial teaches you how to build a CDS service that integrates with EHR systems. You'll learn HealthChain's core concepts by building a real application: + +- **Time**: ~45 minutes +- **Level**: Beginner to Intermediate +- **Prerequisites**: Python basics, familiarity with REST APIs + +[:octicons-arrow-right-24: Start the ClinicalFlow Tutorial](clinicalflow/index.md) + +--- + +## What You'll Learn + +| Tutorial | Core Concepts | +|----------|---------------| +| **ClinicalFlow** | FHIR resources, Document containers, Pipeline components, CDS Hooks gateway, Sandbox testing | + +## Prerequisites + +Before starting any tutorial, make sure you have: + +- Python 3.10 or higher installed +- HealthChain installed (`pip install healthchain`) +- A code editor of your choice +- Basic understanding of Python and REST APIs + +Healthcare knowledge is helpful but not required - the tutorials explain concepts as you go. diff --git a/mkdocs.yml b/mkdocs.yml index 7737e7c2..1239ee8e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,16 @@ nav: - Installation: installation.md - Quickstart: quickstart.md - Licence: distribution.md + - Tutorials: + - tutorials/index.md + - ClinicalFlow Tutorial: + - Introduction: tutorials/clinicalflow/index.md + - Setup: tutorials/clinicalflow/setup.md + - FHIR Basics: tutorials/clinicalflow/fhir-basics.md + - Build Pipeline: tutorials/clinicalflow/pipeline.md + - Create Gateway: tutorials/clinicalflow/gateway.md + - Test with Sandbox: tutorials/clinicalflow/testing.md + - Next Steps: tutorials/clinicalflow/next-steps.md - Cookbook: - cookbook/index.md - Setup FHIR Sandbox: cookbook/setup_fhir_sandboxes.md @@ -82,6 +92,7 @@ nav: copyright: dotimplement theme: name: material + custom_dir: docs/overrides/ favicon: assets/images/healthchain_logo.png logo: assets/images/healthchain_logo.png icon: @@ -90,12 +101,27 @@ theme: - content.code.copy - navigation.expand - navigation.tabs + - navigation.tabs.sticky - navigation.sections + - navigation.footer + - navigation.instant - header.autohide - announce.dismiss + - search.suggest + - search.highlight palette: - primary: white - accent: blue + - scheme: default + primary: white + accent: blue + toggle: + icon: material/white-balance-sunny + name: Switch to dark mode + - scheme: slate + primary: custom + accent: blue + toggle: + icon: material/weather-night + name: Switch to light mode # font: # text: Roboto @@ -132,6 +158,7 @@ markdown_extensions: extra_css: - stylesheets/extra.css + - stylesheets/welcome.css plugins: - blog From 8a4dc612889b014a392f4e7a2bb3a7da62c91124 Mon Sep 17 00:00:00 2001 From: Adam Kells Date: Fri, 16 Jan 2026 11:51:15 +0000 Subject: [PATCH 2/5] More updates --- docs/overrides/welcome.html | 12 +- docs/stylesheets/welcome.css | 256 +++++++++++++++++++++++++++-------- 2 files changed, 209 insertions(+), 59 deletions(-) diff --git a/docs/overrides/welcome.html b/docs/overrides/welcome.html index 021305eb..0c3f522f 100644 --- a/docs/overrides/welcome.html +++ b/docs/overrides/welcome.html @@ -14,7 +14,7 @@

Production-Ready Healthcare AI

@@ -25,7 +25,7 @@

Choose Your Path

- +
@@ -34,12 +34,12 @@

Choose Your Path

HealthTech Engineers

Build clinical workflow integrations with Epic, Cerner, and other EHRs using CDS Hooks and FHIR APIs.

- Start Tutorial > + Start Tutorial
- +
@@ -48,11 +48,12 @@

HealthTech Engineers

LLM / GenAI Developers

Aggregate multi-EHR data for RAG pipelines and build unified patient records with automatic deduplication.

+ View Guide
- + diff --git a/docs/stylesheets/welcome.css b/docs/stylesheets/welcome.css index 4debf984..bff546f2 100644 --- a/docs/stylesheets/welcome.css +++ b/docs/stylesheets/welcome.css @@ -13,6 +13,16 @@ --text-secondary-color: #666666; --accent-color: #e59875; --accent-secondary: #79a8a9; + /* Persona accent colors */ + --persona-healthtech: #e59875; + --persona-healthtech-bg: #fef7f4; + --persona-healthtech-border: #e59875; + --persona-genai: #5c9ead; + --persona-genai-bg: #f0f7f9; + --persona-genai-border: #5c9ead; + --persona-ml: #7c6fb0; + --persona-ml-bg: #f5f3fa; + --persona-ml-border: #7c6fb0; } [data-md-color-scheme="slate"] { @@ -24,6 +34,10 @@ --card-featured-bg-color: #3d2d28; --text-primary-color: #ffffff; --text-secondary-color: #b0b0b0; + /* Dark mode persona colors */ + --persona-healthtech-bg: #3d2d28; + --persona-genai-bg: #263d42; + --persona-ml-bg: #2d2840; } /* Target welcome page container specifically */ @@ -50,60 +64,76 @@ .welcome-title { margin-top: 80px !important; - margin-bottom: 16px !important; + margin-bottom: 20px !important; font-weight: 700 !important; - font-size: 2.5rem !important; + font-size: 3.25rem !important; color: var(--text-primary-color); - line-height: 1.2; + line-height: 1.15; + letter-spacing: -0.02em; } .welcome-subtitle { - font-size: 1.125rem; + font-size: 1.25rem; color: var(--text-secondary-color); - max-width: 600px; - margin: 0 auto 24px auto; + max-width: 640px; + margin: 0 auto 32px auto; line-height: 1.6; } /* Explore/CTA Link */ .explore-link { text-align: center; - margin: 32px 0 64px 0; + margin: 40px 0 72px 0; } .explore-link a { display: inline-flex; align-items: center; - padding: 12px 24px; + padding: 16px 32px; background-color: var(--accent-color); color: #ffffff !important; text-decoration: none !important; - border-radius: 8px; + border-radius: 10px; font-weight: 600; - font-size: 1rem; - transition: background-color 0.2s, transform 0.2s; + font-size: 1.1rem; + transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s; + box-shadow: 0 4px 14px -4px rgba(229, 152, 117, 0.5); } .explore-link a:hover { background-color: #d4845f; transform: translateY(-2px); + box-shadow: 0 6px 20px -4px rgba(229, 152, 117, 0.6); } .explore-link .arrow { - margin-left: 8px; - font-size: 1.2rem; + margin-left: 12px; + font-size: 1.25rem; + transition: transform 0.2s; +} + +.explore-link a:hover .arrow { + transform: translateX(3px); } /* Cards Section Wrapper */ .cards-section-wrapper { background-color: var(--cards-section-bg-color); width: 100%; - padding: 48px 0 64px 0; + padding: 0 0 72px 0; +} + +/* First section (Choose Your Path) has distinct background */ +.cards-section-wrapper section:first-child { + background-color: var(--welcome-page-bg-color); + padding-top: 56px; + padding-bottom: 56px; + margin-bottom: 0; } .cards-section-wrapper section { - margin-bottom: 48px; - padding: 0 16px; + margin-bottom: 64px; + padding: 56px 24px 0 24px; } .cards-section-wrapper section:last-child { @@ -113,25 +143,26 @@ /* Section Titles */ .section-title { font-weight: 600; - font-size: 1.25rem; + font-size: 1.35rem; text-align: center; - margin-bottom: 24px !important; + margin-bottom: 36px !important; margin-top: 0; color: var(--text-primary-color); + letter-spacing: -0.01em; } /* Card Grid */ .card-grid { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 24px; + gap: 28px; margin: 0 auto; - max-width: 900px; + max-width: 1000px; } .card-grid.two-col { grid-template-columns: repeat(2, 1fr); - max-width: 600px; + max-width: 650px; } /* Card Styles */ @@ -139,16 +170,53 @@ background-color: var(--card-bg-color); border: 2px solid var(--card-border-color); border-radius: 16px; - padding: 24px; - transition: transform 0.2s, box-shadow 0.2s; + padding: 28px; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; text-decoration: none !important; display: flex; flex-direction: column; + min-height: fit-content; } .card:hover { - transform: translateY(-4px); - box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.1); + transform: translateY(-3px); + box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.15); +} + +/* Persona cards get larger padding */ +.card.card-healthtech, +.card.card-genai, +.card.card-ml { + padding: 36px; +} + +/* Persona Cards with accent colors */ +.card.card-healthtech { + border-color: var(--persona-healthtech-border); + background-color: var(--persona-healthtech-bg); + position: relative; +} + +.card.card-healthtech .card-icon svg path { + fill: var(--persona-healthtech); +} + +.card.card-genai { + border-color: var(--persona-genai-border); + background-color: var(--persona-genai-bg); +} + +.card.card-genai .card-icon svg path { + fill: var(--persona-genai); +} + +.card.card-ml { + border-color: var(--persona-ml-border); + background-color: var(--persona-ml-bg); +} + +.card.card-ml .card-icon svg path { + fill: var(--persona-ml); } /* Featured Card (Primary Persona) */ @@ -161,31 +229,46 @@ .card.card-featured::before { content: "Recommended"; position: absolute; - top: -12px; - left: 16px; + top: -14px; + left: 24px; background-color: var(--accent-color); color: #ffffff; - font-size: 0.75rem; + font-size: 0.85rem; font-weight: 600; - padding: 4px 12px; - border-radius: 12px; + padding: 6px 16px; + border-radius: 14px; + box-shadow: 0 3px 10px -2px rgba(229, 152, 117, 0.5); + letter-spacing: 0.01em; } /* Card Icon */ .card-icon { - margin-bottom: 16px; - width: 48px; - height: 48px; + margin-bottom: 18px; + width: 56px; + height: 56px; display: flex; align-items: center; justify-content: center; background-color: var(--cards-section-bg-color); - border-radius: 12px; + border-radius: 14px; +} + +/* Persona cards have white icon backgrounds */ +.card.card-healthtech .card-icon, +.card.card-genai .card-icon, +.card.card-ml .card-icon { + background-color: rgba(255, 255, 255, 0.7); +} + +[data-md-color-scheme="slate"] .card.card-healthtech .card-icon, +[data-md-color-scheme="slate"] .card.card-genai .card-icon, +[data-md-color-scheme="slate"] .card.card-ml .card-icon { + background-color: rgba(0, 0, 0, 0.2); } .card-icon svg { - width: 28px; - height: 28px; + width: 32px; + height: 32px; } .card-icon svg path { @@ -200,26 +283,56 @@ } .card-title { - font-size: 1rem !important; + font-size: 1.15rem !important; font-weight: 600 !important; color: var(--text-primary-color); - margin-bottom: 8px !important; + margin-bottom: 12px !important; margin-top: 0 !important; + line-height: 1.3; } .card-description { - font-size: 0.875rem; + font-size: 1rem; color: var(--text-secondary-color); - line-height: 1.5; + line-height: 1.6; margin: 0; flex: 1; } +/* Ensure persona card descriptions are fully visible */ +.card.card-healthtech .card-description, +.card.card-genai .card-description, +.card.card-ml .card-description { + font-size: 0.975rem; + line-height: 1.55; +} + .card-cta { - margin-top: 16px; - font-size: 0.875rem; + margin-top: 24px; + font-size: 0.95rem; font-weight: 600; color: var(--accent-color); + display: inline-flex; + align-items: center; + transition: color 0.2s ease; +} + +/* Persona-specific CTA colors */ +.card.card-healthtech .card-cta { color: var(--persona-healthtech); } +.card.card-genai .card-cta { color: var(--persona-genai); } +.card.card-ml .card-cta { color: var(--persona-ml); } + +.card-cta .arrow { + margin-left: 8px; + transition: transform 0.2s ease; +} + +.card:hover .card-cta .arrow { + transform: translateX(4px); +} + +.card:hover .card-cta { + opacity: 0.85; } /* Welcome Page Footer */ @@ -245,45 +358,80 @@ .card-grid, .card-grid.two-col { grid-template-columns: 1fr; - gap: 20px; + gap: 24px; max-width: 100%; padding: 0 8px; } .cards-section-wrapper { - padding: 32px 0 48px 0; + padding: 0 0 48px 0; + } + + .cards-section-wrapper section:first-child { + padding-top: 40px; + padding-bottom: 40px; } .cards-section-wrapper section { - padding: 0 8px; + padding: 40px 16px 0 16px; + margin-bottom: 0; } .card { - padding: 20px; + padding: 24px; + } + + .card.card-healthtech, + .card.card-genai, + .card.card-ml { + padding: 28px; + } + + .card-icon { + width: 48px; + height: 48px; + } + + .card-icon svg { + width: 28px; + height: 28px; } .welcome-title { margin-top: 48px !important; - font-size: 1.75rem !important; + font-size: 2rem !important; } .welcome-subtitle { - font-size: 1rem; + font-size: 1.05rem; } .explore-link { - margin: 24px 0 48px 0; + margin: 28px 0 48px 0; + } + + .explore-link a { + padding: 14px 28px; + font-size: 1rem; } .section-title { - margin-top: 24px; - margin-bottom: 20px !important; + margin-top: 0; + margin-bottom: 24px !important; } .card.card-featured::before { - top: -10px; - font-size: 0.7rem; - padding: 3px 10px; + top: -12px; + font-size: 0.75rem; + padding: 5px 12px; + } + + .card-description { + font-size: 0.95rem; + } + + .card-title { + font-size: 1.1rem !important; } } From 685bc832a8fb92b95ee3ac0fad311b20f3ac55c4 Mon Sep 17 00:00:00 2001 From: Adam Kells Date: Fri, 16 Jan 2026 16:40:39 +0000 Subject: [PATCH 3/5] Improve dark mode support and navigation styling - Add comprehensive dark mode styles for documentation pages - Fix welcome page to fill full viewport in dark mode - Update persona cards to use neutral backgrounds with colored left borders - Add Kedro-inspired navigation bar with larger tabs and underline hover - Add color accents (coral/teal) to light mode for headings, blockquotes, tables - Ensure WCAG AAA text contrast compliance in dark mode - Restore dark code block backgrounds Co-Authored-By: Claude Opus 4.5 --- docs/stylesheets/extra.css | 483 ++++++++++++++++++++++++++--------- docs/stylesheets/welcome.css | 88 ++++++- 2 files changed, 438 insertions(+), 133 deletions(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 405a0d4f..3ef25d40 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,125 +1,360 @@ @font-face { - font-family: 'Source Code Pro Custom', monospace; - src: url(https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap); - } - - :root > * { - --md-default-bg-color: #FFFFFF; - --md-code-bg-color: #2E3440; - --md-code-fg-color: #FFFFFF; - --md-text-font-family: "Roboto"; - --md-code-font: "Source Code Pro Custom"; - --md-default-fg-color--light: #D8DEE9; - --md-default-fg-color--lighter: #E5E9F0; - --md-default-fg-color--lightest: #ECEFF4; - } - - .index-pre-code { - max-width: 700px; - left: 50%; - } - - .index-pre-code pre>code { - text-align: left; - } - - .md-typeset pre>code { - border-radius: .2rem; - box-shadow: 10px 5px 5px #D8DEE9; - } - - .md-typeset p > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset strong > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-content p > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset td > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset li > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset code { - font-weight: 500; - } - - .md-typeset pre { - margin-left: .5rem; - margin-right: .5rem; - margin-top: 2rem; - margin-bottom: 2rem; - } - - /* .language-python { - background: #FFFFFF ! important - } */ - - .language-bash { - background: #FFFFFF ! important - } - - h1.title { - color: #FFFFFF; - margin: 0px 0px 5px; - } - - h2.subtitle { - margin: 5px 0px 25px; - } - - .md-typeset { - line-height: 24px; - font-weight: 400; - } - - .md-typeset h1 { - font-weight: bold; - color: #000000; - } - - .md-typeset h2 { - font-weight: bold; - color: #000000; - } - - .md-typeset h3 { - font-weight: bold; - color: #000000; - } - - .md-nav__link--active { - background-color: #ECEFF4; - } - - code { - white-space : pre-wrap !important; - } - - .md-content a { - color: #ed4e80; /* Default link color */ - text-decoration: none; /* Remove underline by default */ - transition: all 0.2s ease-in-out; /* Smooth transition for hover effect */ - } - - .md-content a:hover { - color: #2e78d8; /* Darker shade for hover state */ - } + font-family: 'Source Code Pro Custom', monospace; + src: url(https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap); +} + +/* ============================================ + Light Mode (Default) + ============================================ */ +:root { + --md-code-bg-color: #2E3440; + --md-code-fg-color: #FFFFFF; + --md-text-font-family: "Roboto"; + --md-code-font: "Source Code Pro Custom"; + --hc-heading-color: #1a1a1a; + --hc-inline-code-bg: #ECEFF4; + --hc-inline-code-color: #1a1a1a; + --hc-code-shadow: #D8DEE9; + --hc-nav-active-bg: #ECEFF4; + --hc-link-color: #ed4e80; + --hc-link-hover-color: #2e78d8; +} + +/* ============================================ + Dark Mode (Slate) + ============================================ */ +[data-md-color-scheme="slate"] { + --md-default-bg-color: #1a1a1a; + --md-default-fg-color: #ffffff; + --md-default-fg-color--light: #d1d5db; + --md-default-fg-color--lighter: #9ca3af; + --md-default-fg-color--lightest: #4b5563; + --hc-heading-color: #ffffff; + --hc-inline-code-bg: #374151; + --hc-inline-code-color: #e5e7eb; + --hc-code-shadow: transparent; + --hc-nav-active-bg: #374151; + --hc-link-color: #f472b6; + --hc-link-hover-color: #60a5fa; +} + +/* ============================================ + General Styles + ============================================ */ +.index-pre-code { + max-width: 700px; + left: 50%; +} + +.index-pre-code pre>code { + text-align: left; +} + +.md-typeset pre>code { + border-radius: .2rem; + box-shadow: 10px 5px 5px #D8DEE9; + background-color: #2E3440; + color: #FFFFFF; +} + +[data-md-color-scheme="slate"] .md-typeset pre>code { + box-shadow: none; +} + +.md-typeset p > code, +.md-typeset strong > code, +.md-typeset td > code, +.md-typeset li > code { + background: var(--hc-inline-code-bg); + color: var(--hc-inline-code-color); + font-weight: 500; +} + +.md-content p > code { + background: var(--hc-inline-code-bg); + color: var(--hc-inline-code-color); + font-weight: 500; +} + +.md-typeset code { + font-weight: 500; +} + +.md-typeset pre { + margin-left: .5rem; + margin-right: .5rem; + margin-top: 2rem; + margin-bottom: 2rem; +} + +h1.title { + color: #FFFFFF; + margin: 0px 0px 5px; +} + +h2.subtitle { + margin: 5px 0px 25px; +} + +.md-typeset { + line-height: 24px; + font-weight: 400; +} + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3 { + font-weight: bold; + color: var(--hc-heading-color); +} + +.md-nav__link--active { + background-color: var(--hc-nav-active-bg); +} + +code { + white-space: pre-wrap !important; +} + +.md-content a { + color: var(--hc-link-color); + text-decoration: none; + transition: all 0.2s ease-in-out; +} + +.md-content a:hover { + color: var(--hc-link-hover-color); +} + +/* ============================================ + Dark Mode Specific Overrides + ============================================ */ + +/* Ensure dark background applies to all main areas */ +[data-md-color-scheme="slate"] { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] body { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-main { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-main__inner { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-content { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-content__inner { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-sidebar { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-sidebar__scrollwrap { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-container { + background-color: #1a1a1a; +} + +/* Footer dark mode */ +[data-md-color-scheme="slate"] .md-footer { + background-color: #111827; +} + +[data-md-color-scheme="slate"] .md-footer-meta { + background-color: #111827; +} + +[data-md-color-scheme="slate"] .md-typeset { + color: var(--md-default-fg-color--light); +} + +[data-md-color-scheme="slate"] .md-typeset p, +[data-md-color-scheme="slate"] .md-typeset li, +[data-md-color-scheme="slate"] .md-typeset td { + color: #d1d5db; +} + +/* Dark mode navigation */ +[data-md-color-scheme="slate"] .md-nav__link { + color: #d1d5db; +} + +[data-md-color-scheme="slate"] .md-nav__link:hover { + color: #ffffff; +} + +[data-md-color-scheme="slate"] .md-nav__link--active { + color: #ffffff; +} + +/* ============================================ + Top Navigation Bar (Tabs) - Kedro-inspired + ============================================ */ + +/* Header styling */ +.md-header { + padding-left: 24px; + border-radius: 0px 0px 12px 12px; +} + +.md-header--shadow { + box-shadow: none; +} + +/* Tab link styling - larger like Kedro */ +.md-tabs__link { + font-size: 16px; + opacity: 0.85; + transition: opacity 0.2s ease; +} + +.md-tabs__link:hover { + opacity: 1; +} + +/* Active tab - underline effect like Kedro */ +.md-tabs__item--active { + font-weight: 600; +} + +.md-tabs__item--active .md-tabs__link { + opacity: 1; +} + +/* Underline on active tab */ +.md-tabs__item { + border-bottom: 2px solid transparent; + transition: border-color 0.2s ease; +} + +.md-tabs__item:hover { + border-bottom-color: currentColor; +} + +.md-tabs__item--active { + border-bottom: 2px solid currentColor; +} + +/* ============================================ + Light Mode Color Accents + ============================================ */ + +/* Accent color for headings */ +.md-typeset h1 { + color: #1a1a1a; + border-bottom: 3px solid #e59875; + padding-bottom: 0.4rem; + display: inline-block; +} + +/* Colored left border for admonitions */ +.md-typeset .admonition { + border-left: 4px solid #79a8a9; +} + +.md-typeset .admonition.note { + border-left-color: #79a8a9; +} + +.md-typeset .admonition.tip { + border-left-color: #e59875; +} + +.md-typeset .admonition.warning { + border-left-color: #f59e0b; +} + +/* Accent color for blockquotes */ +.md-typeset blockquote { + border-left: 4px solid #e59875; + background-color: #fef7f4; + padding: 1rem 1.5rem; + margin: 1.5rem 0; +} + +/* Table header accent */ +.md-typeset table:not([class]) th { + background-color: #f8f4f2; + border-bottom: 2px solid #e59875; +} + +/* Sidebar accent - active item */ +.md-nav__link--active { + color: #e59875 !important; + font-weight: 600; +} + +.md-nav__item--active > .md-nav__link { + color: #e59875; +} + +/* Search bar styling */ +.md-search__input { + border-radius: 8px; +} + +.md-search__input::placeholder { + color: rgba(255, 255, 255, 0.7); +} + +/* Button/CTA accent colors */ +.md-typeset .md-button { + background-color: #e59875; + border-color: #e59875; + color: #ffffff; +} + +.md-typeset .md-button:hover { + background-color: #d4845f; + border-color: #d4845f; +} + +.md-typeset .md-button--primary { + background-color: #e59875; + border-color: #e59875; +} + +/* Code block header accent */ +.md-typeset .highlight > .filename { + background-color: #f8f4f2; + border-bottom: 2px solid #e59875; +} + +/* ============================================ + Dark Mode Color Accent Overrides + ============================================ */ +[data-md-color-scheme="slate"] .md-typeset h1 { + color: #ffffff; + border-bottom-color: #e59875; +} + +[data-md-color-scheme="slate"] .md-typeset blockquote { + background-color: #1f2937; + border-left-color: #e59875; +} + +[data-md-color-scheme="slate"] .md-typeset table:not([class]) th { + background-color: #1f2937; + border-bottom-color: #e59875; +} + +[data-md-color-scheme="slate"] .md-nav__link--active { + color: #e59875 !important; +} + +[data-md-color-scheme="slate"] .md-typeset .highlight > .filename { + background-color: #1f2937; + border-bottom-color: #e59875; +} diff --git a/docs/stylesheets/welcome.css b/docs/stylesheets/welcome.css index bff546f2..220e48b8 100644 --- a/docs/stylesheets/welcome.css +++ b/docs/stylesheets/welcome.css @@ -26,21 +26,21 @@ } [data-md-color-scheme="slate"] { - --welcome-page-bg-color: #1e1e1e; - --cards-section-bg-color: #2d2d2d; + --welcome-page-bg-color: #1a1a1a; + --cards-section-bg-color: #1f2937; --card-bg-color: #2d2d2d; --card-border-color: #404040; --card-featured-border-color: #e59875; - --card-featured-bg-color: #3d2d28; + --card-featured-bg-color: #2d2d2d; --text-primary-color: #ffffff; - --text-secondary-color: #b0b0b0; - /* Dark mode persona colors */ - --persona-healthtech-bg: #3d2d28; - --persona-genai-bg: #263d42; - --persona-ml-bg: #2d2840; + --text-secondary-color: #d1d5db; + /* Dark mode persona colors - neutral backgrounds with colored left borders */ + --persona-healthtech-bg: #2d2d2d; + --persona-genai-bg: #2d2d2d; + --persona-ml-bg: #2d2d2d; } -/* Target welcome page container specifically */ +/* Target welcome page container and parent elements */ .md-content__inner:has(.welcome-page-container) { padding: 0 !important; margin: 0 !important; @@ -48,12 +48,28 @@ background-color: var(--welcome-page-bg-color); } +/* Ensure full-width background on welcome page */ +.md-main:has(.welcome-page-container) { + background-color: var(--welcome-page-bg-color); +} + +.md-main__inner:has(.welcome-page-container) { + max-width: none; +} + +.md-content:has(.welcome-page-container) { + max-width: none; + background-color: var(--welcome-page-bg-color); +} + /* Main Container */ .welcome-page-container { width: 100%; + min-height: 100vh; display: flex; flex-direction: column; align-items: center; + background-color: var(--welcome-page-bg-color); } /* Hero Section */ @@ -266,6 +282,60 @@ background-color: rgba(0, 0, 0, 0.2); } +/* Dark mode: Remove full colored borders, use subtle left accent borders */ +[data-md-color-scheme="slate"] .card.card-healthtech, +[data-md-color-scheme="slate"] .card.card-genai, +[data-md-color-scheme="slate"] .card.card-ml { + border: 1px solid #404040; + border-left: 3px solid; +} + +[data-md-color-scheme="slate"] .card.card-healthtech { + border-left-color: #e59875; +} + +[data-md-color-scheme="slate"] .card.card-genai { + border-left-color: #5c9ead; +} + +[data-md-color-scheme="slate"] .card.card-ml { + border-left-color: #7c6fb0; +} + +/* Dark mode featured card styling */ +[data-md-color-scheme="slate"] .card.card-featured { + border: 1px solid #404040; + border-left: 3px solid #e59875; +} + +/* Dark mode text contrast - WCAG AAA compliance */ +[data-md-color-scheme="slate"] .card-title, +[data-md-color-scheme="slate"] .card.card-healthtech .card-title, +[data-md-color-scheme="slate"] .card.card-genai .card-title, +[data-md-color-scheme="slate"] .card.card-ml .card-title { + color: #ffffff; +} + +[data-md-color-scheme="slate"] .card-description, +[data-md-color-scheme="slate"] .card.card-healthtech .card-description, +[data-md-color-scheme="slate"] .card.card-genai .card-description, +[data-md-color-scheme="slate"] .card.card-ml .card-description { + color: #d1d5db; +} + +/* Dark mode hero text - ensure high contrast */ +[data-md-color-scheme="slate"] .welcome-title { + color: #ffffff; +} + +[data-md-color-scheme="slate"] .welcome-subtitle { + color: #e5e7eb; +} + +[data-md-color-scheme="slate"] .section-title { + color: #ffffff; +} + .card-icon svg { width: 32px; height: 32px; From f5761a497c529534f03272de623f565f05404e0c Mon Sep 17 00:00:00 2001 From: Adam Kells Date: Fri, 16 Jan 2026 16:52:16 +0000 Subject: [PATCH 4/5] Reduce vertical gap --- docs/stylesheets/extra.css | 11 +++++++++++ docs/stylesheets/welcome.css | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 3ef25d40..c26ff5ba 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -259,6 +259,13 @@ code { display: inline-block; } +/* H2 section styling - left accent border for visual anchoring */ +.md-typeset h2 { + border-left: 3px solid #e59875; + padding-left: 0.75rem; + margin-top: 2.5rem; +} + /* Colored left border for admonitions */ .md-typeset .admonition { border-left: 4px solid #79a8a9; @@ -340,6 +347,10 @@ code { border-bottom-color: #e59875; } +[data-md-color-scheme="slate"] .md-typeset h2 { + border-left-color: #e59875; +} + [data-md-color-scheme="slate"] .md-typeset blockquote { background-color: #1f2937; border-left-color: #e59875; diff --git a/docs/stylesheets/welcome.css b/docs/stylesheets/welcome.css index 220e48b8..b80579ce 100644 --- a/docs/stylesheets/welcome.css +++ b/docs/stylesheets/welcome.css @@ -99,7 +99,7 @@ /* Explore/CTA Link */ .explore-link { text-align: center; - margin: 40px 0 72px 0; + margin: 40px 0 48px 0; } .explore-link a { @@ -142,7 +142,7 @@ /* First section (Choose Your Path) has distinct background */ .cards-section-wrapper section:first-child { background-color: var(--welcome-page-bg-color); - padding-top: 56px; + padding-top: 40px; padding-bottom: 56px; margin-bottom: 0; } From af6a29862e7b9316f70e0d5d88c3ca6da58bffd1 Mon Sep 17 00:00:00 2001 From: Adam Kells Date: Fri, 16 Jan 2026 17:43:04 +0000 Subject: [PATCH 5/5] Fix cookbook filter clear button not working Define clearAllFilters function before attaching event listener to avoid undefined reference. Function expressions assigned to window are not hoisted, so the handler was undefined when listener attached. Co-Authored-By: Claude Opus 4.5 --- docs/cookbook/index.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index dd2f5629..fe5a84e8 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -147,13 +147,14 @@ Hands-on, production-ready examples for building healthcare AI applications with }); }); - clearBtn.addEventListener('click', clearAllFilters); - - window.clearAllFilters = function() { + function clearAllFilters() { activeFilters.clear(); filterTags.forEach(tag => tag.classList.remove('active')); updateCards(); - }; + } + + clearBtn.addEventListener('click', clearAllFilters); + window.clearAllFilters = clearAllFilters; })();