diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/demo/__init__.py b/app/demo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/demo/index.html b/app/demo/index.html
new file mode 100644
index 0000000..c01efa5
--- /dev/null
+++ b/app/demo/index.html
@@ -0,0 +1,526 @@
+
+
+
+
+
+Input dataset · Dimension Reduction Sandbox
+
+
+
+
+
+ §1
+ Select input dataset
+
+ Demo · picker
+
+
+
+ Three candidate generators for the embedding pipeline. Drag to rotate, scroll to zoom,
+ 1 2 3 to select.
+
+
+
+
+
+ 500
+
+
+
+
+
+
+
+
+
+
diff --git a/app/demo/main.py b/app/demo/main.py
new file mode 100644
index 0000000..d6b6d94
--- /dev/null
+++ b/app/demo/main.py
@@ -0,0 +1,64 @@
+from functools import lru_cache
+from pathlib import Path
+
+from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
+from sklearn.datasets import make_blobs, make_s_curve, make_swiss_roll
+
+app = FastAPI()
+HERE = Path(__file__).parent
+
+
+@lru_cache(maxsize=1)
+def _datasets():
+ s, sl = make_s_curve(n_samples=5000, noise=0.03, random_state=0)
+ sr, srl = make_swiss_roll(n_samples=5000, noise=0.15, random_state=0)
+ b, bl = make_blobs(
+ n_samples=5000, n_features=3, centers=5, cluster_std=1.0, random_state=0
+ )
+ return {
+ "s_curve": {
+ "name": "S-Curve",
+ "path": "sklearn.datasets.make_s_curve",
+ "description": (
+ "A 2-D manifold warped into R³. Continuous label encodes position "
+ "along the curve — a good test of whether a reducer unrolls the "
+ "sheet without tearing."
+ ),
+ "kind": "continuous",
+ "points": s.tolist(),
+ "labels": sl.tolist(),
+ },
+ "swiss_roll": {
+ "name": "Swiss Roll",
+ "path": "sklearn.datasets.make_swiss_roll",
+ "description": (
+ "A rolled-up plane. The canonical hard case for linear methods: "
+ "PCA collapses the spiral, non-linear methods should recover the "
+ "unroll."
+ ),
+ "kind": "continuous",
+ "points": sr.tolist(),
+ "labels": srl.tolist(),
+ },
+ "blobs": {
+ "name": "Gaussian Blobs",
+ "path": "sklearn.datasets.make_blobs",
+ "description": (
+ "Five isotropic Gaussian clusters in R³. Discrete class labels. "
+ "Tests whether a reducer preserves cluster separation when "
+ "projected to 2-D."
+ ),
+ "kind": "categorical",
+ "points": b.tolist(),
+ "labels": bl.tolist(),
+ },
+ }
+
+
+@app.get("/data.json")
+def data():
+ return _datasets()
+
+
+app.mount("/", StaticFiles(directory=str(HERE), html=True), name="static")