Skip to content

Commit ba8c6fa

Browse files
committed
docs(demo): host the his-demo as a static SPA at /fhir-dsl/demo
Bring the Caduceus HIS demo onto the docs site. Patient list/detail, FHIRPath playground, and the SMART OAuth flow all work statically; the chatbot drawer is hidden in this mode because /api/chat + /api/mcp need a server runtime. Build pipeline: - apps/his-demo/vite.config.ts gains a DEMO_STATIC=1 + DEMO_BASE_PATH env-var-driven branch that swaps the Nitro plugins for SPA mode and prerendering. Normal pnpm dev / pnpm demo:gen flows are unchanged. - apps/his-demo/src/router.tsx reads import.meta.env.BASE_URL and passes it as TanStack Router's basepath so client-side navigation stays correct under /fhir-dsl/demo/. - apps/his-demo/src/routes/__root.tsx hides the chat trigger + drawer whenever BASE_URL !== '/' (i.e. in any subpath / static deploy). - .github/workflows/deploy-docs.yml: after Docusaurus builds, gen R4 types, build the demo with DEMO_STATIC=1, copy dist/client into apps/docs/build/demo/, and dual-publish _shell.html as both index.html (root) and 404.html (so deep routes fall through to the SPA shell). - apps/docs adds a /docs/demo page describing what the demo shows + how to run the full chatbot version locally, plus a "Demo" navbar entry using pathname:///demo/ to bypass Docusaurus's broken-link checker (the demo lives outside the docs tree).
1 parent 4a485dd commit ba8c6fa

7 files changed

Lines changed: 141 additions & 15 deletions

File tree

.github/workflows/deploy-docs.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
branches: [main]
66
paths:
77
- 'apps/docs/**'
8+
- 'apps/his-demo/**'
9+
- 'packages/**'
810
- '.github/workflows/deploy-docs.yml'
911
workflow_dispatch:
1012

@@ -40,6 +42,31 @@ jobs:
4042
working-directory: apps/docs
4143
run: pnpm build
4244

45+
- name: Generate FHIR types for demo
46+
working-directory: apps/his-demo
47+
run: pnpm gen
48+
49+
- name: Build demo (static SPA)
50+
working-directory: apps/his-demo
51+
env:
52+
DEMO_STATIC: '1'
53+
DEMO_BASE_PATH: /fhir-dsl/demo/
54+
run: pnpm build
55+
56+
- name: Stage demo into docs build
57+
run: |
58+
set -euo pipefail
59+
DEMO_SRC=apps/his-demo/dist/client
60+
DEMO_DST=apps/docs/build/demo
61+
mkdir -p "$DEMO_DST"
62+
cp -R "$DEMO_SRC"/. "$DEMO_DST"/
63+
# GitHub Pages serves /demo/ → /demo/index.html (root-route SPA shell);
64+
# /demo/<deep-route> → /demo/404.html (which is the same SPA shell, so the
65+
# client-side router handles the deep route after hydration).
66+
cp "$DEMO_DST/_shell.html" "$DEMO_DST/index.html"
67+
cp "$DEMO_DST/_shell.html" "$DEMO_DST/404.html"
68+
echo "demo size: $(du -sh $DEMO_DST | cut -f1)"
69+
4370
- name: Upload artifact
4471
uses: actions/upload-pages-artifact@v3
4572
with:

apps/docs/docs/demo.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
id: demo
3+
title: Live Demo — Caduceus HIS
4+
description: A working hospital information system built with fhir-dsl — typed search, FHIRPath playground, SMART-on-FHIR patient launch, and a Claude chatbot wired through MCP.
5+
sidebar_label: Live Demo
6+
sidebar_position: 100
7+
---
8+
9+
# Live Demo — Caduceus HIS
10+
11+
**Caduceus** is a hospital information system demo that uses **every published `@fhir-dsl/*` package** end-to-end. It's the surface-freeze smoke test for v1.x — and the most concrete way to see what the toolchain looks like in a real React app.
12+
13+
**[Live demo](pathname:///demo/)** &nbsp;·&nbsp; **[Source on GitHub](https://github.com/awbx/fhir-dsl/tree/main/apps/his-demo)**
14+
15+
## What's in the demo
16+
17+
| Pillar | What it shows | Packages exercised |
18+
|---|---|---|
19+
| **Patient list / detail** | Typed search with `where` / `include` / `sort`, profile-narrowing toggle (US Core), FHIRPath-driven write-back via JSON Patch, optimistic update + rollback on `FhirRequestError` | `@fhir-dsl/core`, `@fhir-dsl/runtime`, `@fhir-dsl/fhirpath`, `@fhir-dsl/tanstack-query` |
20+
| **SMART-on-FHIR launch** | Full v2 flow through the SMART Health IT sandbox: discovery → PKCE → code exchange → token persistence | `@fhir-dsl/smart` |
21+
| **FHIRPath playground** | Live compile + evaluate against sample resources, UCUM-aware Quantity demo, terminology hooks demo | `@fhir-dsl/fhirpath` |
22+
| **Claude chatbot (full version only)** | LLM tool-use loop wired to an in-process MCP server, with audit timeline | `@fhir-dsl/mcp`, `@anthropic-ai/sdk` |
23+
24+
## Two deployment flavors
25+
26+
The demo is a TanStack Start app with both client and server routes. We ship two builds:
27+
28+
- **GitHub Pages (this site, `/demo/`)** — a **static SPA build**. Patient list/detail, FHIRPath playground, and the SMART login flow all work because they're client-only. **The chatbot is disabled** because it requires server-side calls to the Anthropic SDK and the in-process MCP dispatcher.
29+
- **Full self-host** — build from the source repo and run with `pnpm demo` for the complete experience including the chatbot. Requires `ANTHROPIC_API_KEY` in your environment.
30+
31+
## Run the full version locally
32+
33+
```bash
34+
git clone https://github.com/awbx/fhir-dsl.git
35+
cd fhir-dsl
36+
pnpm install
37+
pnpm demo:gen # generates R4 FHIR types into apps/his-demo/src/fhir/r4
38+
pnpm demo # runs the dev server with chatbot + MCP enabled
39+
```
40+
41+
Open `http://localhost:3000` and click **"Login with SMART"** to start the patient-launch dance. Drop your `ANTHROPIC_API_KEY` in `apps/his-demo/.env.local` first if you want the chatbot.
42+
43+
## Architecture
44+
45+
The demo is structured as a **TanStack Start app** with file-based routing and shadcn/ui. The interesting bits to read:
46+
47+
- [`src/routes/patients.$id.tsx`](https://github.com/awbx/fhir-dsl/blob/main/apps/his-demo/src/routes/patients.%24id.tsx) — typed search, US Core profile narrowing toggle, FHIRPath write-back via JSON Patch
48+
- [`src/routes/playground.tsx`](https://github.com/awbx/fhir-dsl/blob/main/apps/his-demo/src/routes/playground.tsx) — Monaco editor wired to live FHIRPath compile/evaluate
49+
- [`src/server/mcp-singleton.ts`](https://github.com/awbx/fhir-dsl/blob/main/apps/his-demo/src/server/mcp-singleton.ts)`@fhir-dsl/mcp` dispatcher mounted in-process on the TanStack Start server
50+
- [`src/server/claude.ts`](https://github.com/awbx/fhir-dsl/blob/main/apps/his-demo/src/server/claude.ts) — Anthropic SDK tool-use loop bound to the MCP dispatcher (max 8 tool-call steps per turn)
51+
- [`src/lib/use-fhir-client.ts`](https://github.com/awbx/fhir-dsl/blob/main/apps/his-demo/src/lib/use-fhir-client.ts)`createClient` wired to a `SmartClient` `AuthProvider`
52+
53+
## Related
54+
55+
- [Architecture overview](./architecture/overview.md) — how the eleven packages compose
56+
- [SMART on FHIR guide](./guides/smart.md) — the auth flow the demo exercises
57+
- [MCP guide](./guides/mcp.md) — the chatbot's tool surface
58+
- [`@fhir-dsl/tanstack-query`](./api/tanstack-query.md) — the React Query bindings every page in the demo uses

apps/docs/docusaurus.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,15 @@ const config = {
173173
position: 'left',
174174
label: 'Docs',
175175
},
176+
{
177+
// pathname:// prefix bypasses Docusaurus's broken-link checker —
178+
// the demo lives outside the Docusaurus tree (built separately and
179+
// copied into build/demo/ by the deploy workflow).
180+
href: 'pathname:///demo/',
181+
label: 'Demo',
182+
position: 'left',
183+
target: '_self',
184+
},
176185
{
177186
href: 'https://github.com/awbx/fhir-dsl',
178187
label: 'GitHub',

apps/docs/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const sidebars = {
8484
'recipes/conditional-transaction',
8585
],
8686
},
87+
'demo',
8788
'edge-cases',
8889
'llm-usage',
8990
{

apps/his-demo/src/router.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import { getContext } from './integrations/tanstack-query/root-provider'
77
export function getRouter() {
88
const context = getContext()
99

10+
// Strip trailing slash so basepath '/fhir-dsl/demo/' becomes '/fhir-dsl/demo'.
11+
// TanStack Router expects no trailing slash; '/' stays as '/' for normal builds.
12+
const rawBase = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '')
13+
const basepath = rawBase === '' ? '/' : rawBase
14+
1015
const router = createTanStackRouter({
1116
routeTree,
1217
context,
18+
basepath,
1319
scrollRestoration: true,
1420
defaultPreload: 'intent',
1521
defaultPreloadStaleTime: 0,

apps/his-demo/src/routes/__root.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ interface MyRouterContext {
2020
queryClient: QueryClient;
2121
}
2222

23+
// In static SPA builds (GitHub Pages at /fhir-dsl/demo) the /api/chat and
24+
// /api/mcp server routes don't exist. Hide the chatbot UI to keep the demo
25+
// honest about what's actually working.
26+
const IS_STATIC = import.meta.env.BASE_URL !== "/";
27+
2328
export const Route = createRootRouteWithContext<MyRouterContext>()({
2429
head: () => ({
2530
meta: [
@@ -40,7 +45,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
4045
</head>
4146
<body className="min-h-dvh bg-background text-foreground antialiased">
4247
<AppShell>{children}</AppShell>
43-
<ChatDrawer />
48+
{IS_STATIC ? null : <ChatDrawer />}
4449
<TanStackDevtools
4550
config={{ position: "bottom-right" }}
4651
plugins={[
@@ -81,13 +86,15 @@ function AppShell({ children }: { children: React.ReactNode }) {
8186
>
8287
Playground
8388
</Link>
84-
<button
85-
type="button"
86-
className="flex items-center gap-1 rounded border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted"
87-
data-chatbot-trigger
88-
>
89-
<MessageSquare className="h-3.5 w-3.5" /> Chat
90-
</button>
89+
{IS_STATIC ? null : (
90+
<button
91+
type="button"
92+
className="flex items-center gap-1 rounded border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted"
93+
data-chatbot-trigger
94+
>
95+
<MessageSquare className="h-3.5 w-3.5" /> Chat
96+
</button>
97+
)}
9198
</nav>
9299
</div>
93100
</header>

apps/his-demo/vite.config.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,33 @@ import viteReact from '@vitejs/plugin-react'
77
import tailwindcss from '@tailwindcss/vite'
88
import { nitro } from 'nitro/vite'
99

10+
// Set DEMO_STATIC=1 to build a static SPA (no server routes) for hosting on
11+
// GitHub Pages at https://awbx.github.io/fhir-dsl/demo. The chatbot drawer is
12+
// disabled in this mode because /api/chat + /api/mcp need a server runtime.
13+
const STATIC_BUILD = process.env.DEMO_STATIC === '1'
14+
const BASE_PATH = process.env.DEMO_BASE_PATH ?? '/'
15+
1016
const config = defineConfig({
17+
base: BASE_PATH,
1118
resolve: { tsconfigPaths: true },
12-
plugins: [
13-
devtools(),
14-
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
15-
tailwindcss(),
16-
tanstackStart(),
17-
viteReact(),
18-
],
19+
plugins: STATIC_BUILD
20+
? [
21+
tailwindcss(),
22+
tanstackStart({
23+
spa: {
24+
enabled: true,
25+
prerender: { enabled: true },
26+
},
27+
}),
28+
viteReact(),
29+
]
30+
: [
31+
devtools(),
32+
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
33+
tailwindcss(),
34+
tanstackStart(),
35+
viteReact(),
36+
],
1937
})
2038

2139
export default config

0 commit comments

Comments
 (0)