@emseepea/create-html-ui-server 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,6 +26,12 @@ Protocol (MCP) server. It uses the native renderer and the same sample states as
26
26
  the React example. Choose the React example instead when your application
27
27
  already uses React and needs client-side updates.
28
28
 
29
+ `src/server.ts` assembles the server. HTTP handlers live in `src/routes/`, where
30
+ the filename supplies the method and path. `get.index.ts` serves the page,
31
+ `post.index.ts` handles the form, and `get.emseepea.css.ts` serves the
32
+ stylesheet. Add a direct Fastify route only when a handler does not fit this
33
+ simple file convention.
34
+
29
35
  ## Run
30
36
 
31
37
  From this directory:
@@ -8,6 +8,12 @@ Protocol (MCP) server. It uses the native renderer and the same sample states as
8
8
  the React example. Choose the React example instead when your application
9
9
  already uses React and needs client-side updates.
10
10
 
11
+ `src/server.ts` assembles the server. HTTP handlers live in `src/routes/`, where
12
+ the filename supplies the method and path. `get.index.ts` serves the page,
13
+ `post.index.ts` handles the form, and `get.emseepea.css.ts` serves the
14
+ stylesheet. Add a direct Fastify route only when a handler does not fit this
15
+ simple file convention.
16
+
11
17
  ## Run
12
18
 
13
19
  From this directory:
@@ -1,21 +1,28 @@
1
- import { toolSelectionTest } from "@emseepea/testing/semantic";
1
+ import test from "node:test";
2
+ import {
3
+ assertResponseContains,
4
+ assertResponseMeaning,
5
+ assertToolCalls,
6
+ createConversation,
7
+ } from "@emseepea/testing/semantic";
2
8
 
3
- toolSelectionTest("Native UI preview is not mistaken for a completed effect", {
4
- server: new URL("../dist/server.js", import.meta.url),
5
- question:
6
- "Summarize the snap-pea planting-plan preview. Was a report sent or stored, and did this " +
7
- "operation change anything?",
8
- criticalFacts: [
9
- "Highland Snap",
10
- "Meadow Sweet",
11
- "preview-only",
12
- "false",
13
- /\b(?:no|not|nothing|neither|never|wasn't|weren't|didn't)\b[^.!?\n]{0,80}\b(?:sent|delivered)\b/i,
14
- /\b(?:no|not|nothing|neither|never|wasn't|weren't|didn't)\b[^.!?\n]{0,80}\b(?:stored|saved|persisted)\b/i
15
- ],
16
- criteria:
17
- "The answer identifies Highland Snap and Meadow Sweet as the two snap-pea " +
18
- "matches. It says the result is preview-only, effectPerformed is false, no report " +
19
- "was sent or stored, and no external action or data change occurred.",
20
- expectedTools: ["preview-planting-plan"],
9
+ test("does not mistake a native UI preview for a completed effect", async (t) => {
10
+ const chat = await createConversation(t, {
11
+ server: new URL("../dist/server.js", import.meta.url),
12
+ });
13
+ const response = await chat.send(
14
+ "Preview a plan titled Snap pea plan for snap peas, including growing tips. " +
15
+ "Summarize the matches and say whether anything was sent, stored, or changed.",
16
+ );
17
+
18
+ assertToolCalls(response, [{
19
+ name: "preview-planting-plan",
20
+ arguments: { title: "Snap pea plan", peaType: "snap", includeTips: true },
21
+ }]);
22
+ assertResponseContains(response, "Highland Snap");
23
+ await assertResponseMeaning(response, {
24
+ expected:
25
+ "The preview contains Highland Snap and Meadow Sweet. It is preview-only, " +
26
+ "performed no effect, sent and stored no report, and changed no external data.",
27
+ });
21
28
  });
@@ -16,7 +16,7 @@
16
16
  "devDependencies": {
17
17
  "playwright": "1.62.1",
18
18
  "axe-core": "4.13.0",
19
- "@emseepea/testing": "0.2.2",
19
+ "@emseepea/testing": "0.3.0",
20
20
  "@types/node": "24.13.3",
21
21
  "typescript": "6.0.3",
22
22
  "oxlint": "1.80.0"
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "private": true,
28
28
  "dependencies": {
29
- "@emseepea/server": "0.2.2",
29
+ "@emseepea/server": "0.3.0",
30
30
  "@emseepea/tailwind": "0.0.2"
31
31
  }
32
32
  }
@@ -0,0 +1,9 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { HttpRouteHandler } from "@emseepea/server";
4
+
5
+ const stylesheet = await readFile(new URL(import.meta.resolve("@emseepea/tailwind/styles.css")), "utf8");
6
+
7
+ export default (async (_request, reply) => {
8
+ await reply.type("text/css; charset=utf-8").header("cache-control", "no-store").send(stylesheet);
9
+ }) satisfies HttpRouteHandler;
@@ -0,0 +1,7 @@
1
+ import type { HttpRouteHandler } from "@emseepea/server";
2
+
3
+ import { pageFromQuery } from "../ui.js";
4
+
5
+ export default (async (request, reply) => {
6
+ await reply.type("text/html; charset=utf-8").send(pageFromQuery(request.query));
7
+ }) satisfies HttpRouteHandler;
@@ -0,0 +1,7 @@
1
+ import type { HttpRouteHandler } from "@emseepea/server";
2
+
3
+ import { pageFromSubmission } from "../ui.js";
4
+
5
+ export default (async (request, reply) => {
6
+ await reply.type("text/html; charset=utf-8").send(pageFromSubmission(request.body));
7
+ }) satisfies HttpRouteHandler;
@@ -1,13 +1,7 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { parse } from "node:querystring";
3
2
 
4
- import {
5
- fixtureForState,
6
- viewFromSubmission,
7
- } from "./ui-shared.js";
8
- import { createEmseepea, discoverCapabilities, renderElicitationForm, serveEmseepea } from "@emseepea/server";
3
+ import { createEmseepea, discoverCapabilities, registerRoutes, serveEmseepea } from "@emseepea/server";
9
4
 
10
- const stylesheet = await readFile(new URL(import.meta.resolve("@emseepea/tailwind/styles.css")), "utf8");
11
5
  const app = createEmseepea({
12
6
  name: "emseepea-html-ui-server",
13
7
  version: "0.0.0",
@@ -20,20 +14,7 @@ app.addContentTypeParser(
20
14
  { parseAs: "string" },
21
15
  (_request, body, done) => done(null, parse(body.toString())),
22
16
  );
23
- app.get("/emseepea.css", async (_request, reply) => {
24
- await reply.type("text/css; charset=utf-8").header("cache-control", "no-store").send(stylesheet);
25
- });
26
- app.get("/", async (request, reply) => {
27
- const query = record(request.query);
28
- await reply.type("text/html; charset=utf-8").send(page(
29
- fixtureForState(query.state),
30
- first(query.theme) === "dark" ? "dark" : "light",
31
- first(query.style) !== "off",
32
- ));
33
- });
34
- app.post("/", async (request, reply) => {
35
- await reply.type("text/html; charset=utf-8").send(page(viewFromSubmission(request.body), "light", true));
36
- });
17
+ await registerRoutes(app, new URL("./routes/", import.meta.url));
37
18
 
38
19
  const running = await serveEmseepea(app, { port: Number.parseInt(process.env.PORT ?? "3000", 10) });
39
20
  console.log(`Em See Pea native UI example listening at ${running.url}`);
@@ -44,18 +25,3 @@ async function shutdown(): Promise<void> {
44
25
  }
45
26
  process.once("SIGINT", () => void shutdown());
46
27
  process.once("SIGTERM", () => void shutdown());
47
-
48
- function page(view: unknown, theme: "light" | "dark", styled: boolean): string {
49
- return `<!doctype html><html lang="en" data-emseepea-theme="${theme}"><head>` +
50
- `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
51
- `<title>Pea planting plan preview - Em See Pea</title>${styled ? '<link rel="stylesheet" href="/emseepea.css">' : ""}` +
52
- `</head><body><a href="#main-content">Skip to main content</a><main id="main-content" tabindex="-1">` +
53
- `<h1>Native form example</h1>${renderElicitationForm(view, { headingLevel: 2 })}</main></body></html>`;
54
- }
55
-
56
- function record(value: unknown): Record<string, unknown> {
57
- return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
58
- }
59
- function first(value: unknown): string | undefined {
60
- return typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
61
- }
@@ -0,0 +1,31 @@
1
+ import { fixtureForState, viewFromSubmission } from "./ui-shared.js";
2
+ import { renderElicitationForm } from "@emseepea/server";
3
+
4
+ export function pageFromQuery(value: unknown): string {
5
+ const query = record(value);
6
+ return page(
7
+ fixtureForState(query.state),
8
+ first(query.theme) === "dark" ? "dark" : "light",
9
+ first(query.style) !== "off",
10
+ );
11
+ }
12
+
13
+ export function pageFromSubmission(value: unknown): string {
14
+ return page(viewFromSubmission(value), "light", true);
15
+ }
16
+
17
+ function page(view: unknown, theme: "light" | "dark", styled: boolean): string {
18
+ return `<!doctype html><html lang="en" data-emseepea-theme="${theme}"><head>` +
19
+ `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
20
+ `<title>Pea planting plan preview - Em See Pea</title>${styled ? '<link rel="stylesheet" href="/emseepea.css">' : ""}` +
21
+ `</head><body><a href="#main-content">Skip to main content</a><main id="main-content" tabindex="-1">` +
22
+ `<h1>Native form example</h1>${renderElicitationForm(view, { headingLevel: 2 })}</main></body></html>`;
23
+ }
24
+
25
+ function record(value: unknown): Record<string, unknown> {
26
+ return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
27
+ }
28
+
29
+ function first(value: unknown): string | undefined {
30
+ return typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
31
+ }
@@ -23,6 +23,7 @@ export function testUiExample(example) {
23
23
  if (message.type() === "error") errors.push(message.text());
24
24
  });
25
25
  page.on("pageerror", (error) => errors.push(error.message));
26
+ await assertRouteResponses(page.request, running.origin, example.react === true);
26
27
 
27
28
  for (const theme of ["light", "dark"]) {
28
29
  for (const state of ["ready", "invalid", "busy", "terminal"]) {
@@ -35,6 +36,8 @@ export function testUiExample(example) {
35
36
  `${example.name} ${theme} ${state} has axe violations`,
36
37
  );
37
38
  assert.equal(await page.locator("html[lang='en']").count(), 1);
39
+ assert.equal(await page.locator("meta[name='viewport'][content='width=device-width, initial-scale=1']").count(), 1);
40
+ assert.equal(await page.locator("a[href='#main-content']").innerText(), "Skip to main content");
38
41
  assert.equal(await page.locator("main#main-content").count(), 1);
39
42
  assert.equal(await page.locator("h1").count(), 1);
40
43
  assert.equal(await page.title(), "Pea planting plan preview - Em See Pea");
@@ -130,6 +133,32 @@ export function testUiExample(example) {
130
133
  });
131
134
  }
132
135
 
136
+ async function assertRouteResponses(request, origin, react) {
137
+ const document = await request.get(`${origin}/`);
138
+ assert.equal(document.status(), 200);
139
+ assert.match(document.headers()["content-type"], /^text\/html; charset=utf-8$/);
140
+
141
+ const stylesheet = await request.get(`${origin}/emseepea.css`);
142
+ assert.equal(stylesheet.status(), 200);
143
+ assert.match(stylesheet.headers()["content-type"], /^text\/css; charset=utf-8$/);
144
+ assert.equal(stylesheet.headers()["cache-control"], "no-store");
145
+
146
+ const submission = await request.post(`${origin}/`, { form: { title: "Route contract" } });
147
+ assert.equal(submission.status(), 200);
148
+ assert.match(
149
+ submission.headers()["content-type"],
150
+ react ? /^application\/json; charset=utf-8$/ : /^text\/html; charset=utf-8$/,
151
+ );
152
+
153
+ if (react) {
154
+ const client = await request.get(`${origin}/client.js`);
155
+ assert.equal(client.status(), 200);
156
+ assert.match(client.headers()["content-type"], /^text\/javascript; charset=utf-8$/);
157
+ assert.equal(client.headers()["cache-control"], "no-store");
158
+ assert.ok((await client.body()).byteLength > 0);
159
+ }
160
+ }
161
+
133
162
  async function assertNoEffectClaim(page) {
134
163
  const text = await page.locator("[data-emseepea-part='terminal']").innerText();
135
164
  assert.match(text, /No report was sent or stored\./);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/create-html-ui-server",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Create an Em See Pea server with an accessible HTML form.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,12 +18,12 @@
18
18
  "prepack": "npm run build:initializer"
19
19
  },
20
20
  "devDependencies": {
21
- "@emseepea/server": "0.2.2",
21
+ "@emseepea/server": "0.3.0",
22
22
  "@emseepea/tailwind": "0.0.2",
23
23
  "@emseepea/example-ui-shared": "0.0.0",
24
24
  "playwright": "1.62.1",
25
25
  "axe-core": "4.13.0",
26
- "@emseepea/testing": "0.2.2",
26
+ "@emseepea/testing": "0.3.0",
27
27
  "@types/node": "24.13.3",
28
28
  "typescript": "6.0.3",
29
29
  "oxlint": "1.80.0"