@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 +6 -0
- package/initializer-dist/template/README.md +6 -0
- package/initializer-dist/template/eval/meaning.test.mjs +26 -19
- package/initializer-dist/template/package.json +2 -2
- package/initializer-dist/template/src/routes/get.emseepea.css.ts +9 -0
- package/initializer-dist/template/src/routes/get.index.ts +7 -0
- package/initializer-dist/template/src/routes/post.index.ts +7 -0
- package/initializer-dist/template/src/server.ts +2 -36
- package/initializer-dist/template/src/ui.ts +31 -0
- package/initializer-dist/template/test-support/browser-contract.mjs +29 -0
- package/package.json +3 -3
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
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import {
|
|
3
|
+
assertResponseContains,
|
|
4
|
+
assertResponseMeaning,
|
|
5
|
+
assertToolCalls,
|
|
6
|
+
createConversation,
|
|
7
|
+
} from "@emseepea/testing/semantic";
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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.
|
|
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 { 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
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|