Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,43 @@ node test.js
- Add or extend APIs on **`window.LAMBS`** inside `index.html`.
- Document JSON field changes here under **Using LAMBS** (`reportVersion` bumps if breaking).
- Prefer **Copy JSON** / `lambsReportToJSON` over new DOM scraping hooks.

---

## Cursor Cloud specific instructions

LAMBS is a **zero-backend** product: one `index.html` file runs entirely in the browser. No database, API server, or Docker is required.

### Verify logic (CI-equivalent)

```bash
node test.js
```

Requires **Node.js only** — no `pnpm install`. CI uses Node 24; Node 22+ works locally.

There is **no lint command** configured (no ESLint/Prettier in the repo).

### Run the application

Open `index.html` in a browser (`file://` or static server). For a headless UI smoke test covering **both** tabs (Single mAb Analysis + Multiple mAb Analysis with built-in example sequences):

```bash
pnpm install
pnpm exec playwright install chromium # one-time per VM; not in update script
node scripts/verify-ui.mjs
```

This loads the Trastuzumab example on the Single tab, runs **Analyze**, then switches to Multiple mAb Analysis, loads the 4-sequence example CSV, runs **Cluster & Analyze**, and saves screenshots under `LAMBS_UI_OUT` (default `/opt/cursor/artifacts/screenshots`).

Or serve statically: `python3 -m http.server 8080 --directory /workspace` → `http://localhost:8080/index.html`.

### Optional dev dependencies

For README screenshots:

```bash
pnpm screenshots
```

Python 3 scripts under `scripts/` regenerate embedded germline/stats data in `index.html`; only needed when changing those datasets, not for normal dev/test.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "LAMBS - Local Analysis of mAb Sequences",
"scripts": {
"release": "commit-and-tag-version --config ./scripts/version-updater/versionrc.cjs",
"screenshots": "node scripts/capture-readme-screenshots.mjs"
"screenshots": "node scripts/capture-readme-screenshots.mjs",
"verify-ui": "node scripts/verify-ui.mjs"
},
"devDependencies": {
"commit-and-tag-version": "12.7.1",
Expand Down
103 changes: 103 additions & 0 deletions scripts/verify-ui.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Headless browser smoke test for LAMBS UI (Single + Multiple mAb Analysis tabs).
*
* Prerequisites:
* pnpm install
* pnpm exec playwright install chromium
*
* Usage:
* node scripts/verify-ui.mjs
* LAMBS_UI_OUT=/path/to/dir node scripts/verify-ui.mjs
*/
import { mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { chromium } from "playwright";

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, "..");
const INDEX_URL = pathToFileURL(join(REPO_ROOT, "index.html")).href;
const OUT_DIR = process.env.LAMBS_UI_OUT || "/opt/cursor/artifacts/screenshots";

mkdirSync(OUT_DIR, { recursive: true });

function assert(condition, message) {
if (!condition) throw new Error(message);
}

const browser = await chromium.launch({ headless: true });

try {
const page = await browser.newPage({ viewport: { width: 1200, height: 1220 } });
await page.goto(INDEX_URL, { waitUntil: "domcontentloaded" });

// --- Single mAb Analysis ---
console.log("=== Single mAb Analysis tab ===");
await page.evaluate(() => {
switchTab("single");
loadExampleSingle();
analyze();
});
await page
.locator("#results")
.getByText(/germline|VH|VL|liability/i)
.first()
.waitFor({ timeout: 15000 });

const singleApi = await page.evaluate(() => {
const r = LAMBS.analyzeMabReportFromRaw(
document.getElementById("vh-input").value,
document.getElementById("vl-input").value
);
if (!r.ok) return { ok: false, errors: r.errors };
return {
ok: true,
kind: r.report.kind,
vhVGene: r.report.chains.vh.variable.germline.vGene?.gene,
vlVGene: r.report.chains.vl.variable.germline.vGene?.gene,
};
});
console.log(JSON.stringify(singleApi, null, 2));
assert(singleApi.ok && singleApi.kind === "single-mab", "Single tab analysis failed");

const singleShot = join(OUT_DIR, "lambs-single-analysis-demo.png");
await page.screenshot({ path: singleShot, fullPage: false });
console.log("Screenshot:", singleShot);

// --- Multiple mAb Analysis ---
console.log("\n=== Multiple mAb Analysis tab ===");
await page.evaluate(() => {
switchTab("multi");
loadExampleCSV();
runMultiAnalysis();
});
await page
.locator("#multi-results")
.getByText(/cluster|alignment|consensus/i)
.first()
.waitFor({ timeout: 15000 });

const multiApi = await page.evaluate(() => ({
basketCount: typeof basket !== "undefined" ? basket.length : 0,
clusterReport: LAMBS.lastClusterReport
? {
kind: LAMBS.lastClusterReport.kind,
inputCount: LAMBS.lastClusterReport.inputCount,
clusterCount: LAMBS.lastClusterReport.clusterCount,
firstClusterSize: LAMBS.lastClusterReport.clusters?.[0]?.size,
}
: null,
}));
console.log(JSON.stringify(multiApi, null, 2));
assert(multiApi.basketCount === 4, "Expected 4 example sequences in basket");
assert(multiApi.clusterReport?.kind === "cluster", "Multi tab cluster report missing");
assert(multiApi.clusterReport.clusterCount >= 1, "Expected at least one cluster");

const multiShot = join(OUT_DIR, "lambs-multiple-analysis-demo.png");
await page.screenshot({ path: multiShot, fullPage: false });
console.log("Screenshot:", multiShot);

console.log("\nUI verification passed (Single + Multiple mAb Analysis).");
} finally {
await browser.close();
}