@ai-gui/plugin-form 0.7.0 → 0.8.0

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/dist/index.cjs CHANGED
@@ -51,7 +51,8 @@ const FIELD_KEYS = new Set([
51
51
  "min",
52
52
  "max",
53
53
  "options",
54
- "placeholder"
54
+ "placeholder",
55
+ "expect"
55
56
  ]);
56
57
  const OPTION_KEYS = new Set(["label", "value"]);
57
58
  const SAFE_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
@@ -232,12 +233,16 @@ function parseField(value, index, issues) {
232
233
  issues.push(`${path}.pattern must be a valid regular expression.`);
233
234
  }
234
235
  }
236
+ let expected;
237
+ if (value.expect !== void 0) if (typeof value.expect === "string" || typeof value.expect === "number" || typeof value.expect === "boolean") expected = value.expect;
238
+ else issues.push(`${path}.expect must be a string, a number, or a boolean.`);
235
239
  if (!name || !label || !type) return void 0;
236
240
  const base = {
237
241
  name,
238
242
  label,
239
243
  ...value.required === true ? { required: true } : {},
240
- ...placeholder === void 0 ? {} : { placeholder }
244
+ ...placeholder === void 0 ? {} : { placeholder },
245
+ ...expected === void 0 ? {} : { expect: expected }
241
246
  };
242
247
  if (type === "number") {
243
248
  if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || value.options !== void 0) issues.push(`${path} contains constraints unsupported by number fields.`);
@@ -342,13 +347,35 @@ function mountForm(host, definition, options) {
342
347
  submit.textContent = definition.submitLabel ?? "Submit";
343
348
  formElement.appendChild(submit);
344
349
  /**
350
+ * Compare the submission with the answers the fields declared.
351
+ *
352
+ * Only fields carrying `expect` take part, so a plain form is never marked. The tone is the worst
353
+ * of them: one wrong answer among three right ones is not a pass.
354
+ */
355
+ const gradeAgainstExpectations = (values) => {
356
+ const graded = definition.fields.filter((field) => field.expect !== void 0);
357
+ if (graded.length === 0) return void 0;
358
+ const fieldTones = {};
359
+ let wrong = 0;
360
+ for (const field of graded) {
361
+ const submitted$1 = values[field.name];
362
+ const matches = typeof field.expect === "string" && typeof submitted$1 === "string" ? submitted$1.trim() === field.expect.trim() : submitted$1 === field.expect;
363
+ fieldTones[field.name] = matches ? "positive" : "warning";
364
+ if (!matches) wrong += 1;
365
+ }
366
+ return {
367
+ tone: wrong === 0 ? "positive" : "warning",
368
+ fields: fieldTones
369
+ };
370
+ };
371
+ /**
345
372
  * Show how the submission turned out, when the handler judged it.
346
373
  *
347
374
  * The result used to be discarded, so an app that knew the answer was wrong had no way to say so
348
375
  * — the form simply disabled itself and read "Submitted" whether the answer was right or not.
349
376
  */
350
- const applyOutcome = (result) => {
351
- const outcome = (0, __ai_gui_core.actionOutcome)(result);
377
+ const applyOutcome = (result, fallback) => {
378
+ const outcome = (0, __ai_gui_core.actionOutcome)(result) ?? fallback;
352
379
  if (!outcome || disposed) return;
353
380
  formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
354
381
  if (outcome.message) {
@@ -385,6 +412,7 @@ function mountForm(host, definition, options) {
385
412
  pending = true;
386
413
  submit.disabled = true;
387
414
  formElement.setAttribute("aria-busy", "true");
415
+ const graded = gradeAgainstExpectations(validation.values);
388
416
  const settle = () => {
389
417
  if (disposed) return;
390
418
  pending = false;
@@ -400,7 +428,7 @@ function mountForm(host, definition, options) {
400
428
  signal: controller.signal
401
429
  }).then((result) => {
402
430
  markSubmitted();
403
- applyOutcome(result);
431
+ applyOutcome(result, graded);
404
432
  }, (error) => {
405
433
  if (!disposed && !controller.signal.aborted) {
406
434
  actionError.textContent = actionErrorMessage(error);
package/dist/index.d.cts CHANGED
@@ -12,6 +12,15 @@ interface FormFieldBase {
12
12
  label: string;
13
13
  required?: boolean;
14
14
  placeholder?: string;
15
+ /**
16
+ * The answer this field is expected to carry.
17
+ *
18
+ * Declaring it lets the form report how the submission compared — a quiz colours itself the
19
+ * moment it is answered instead of waiting for a round trip to say so. Unlike the constraints
20
+ * beside it this never blocks a submission: a wrong answer is an answer, and the person is told
21
+ * rather than stopped.
22
+ */
23
+ expect?: string | number | boolean;
15
24
  }
16
25
  interface FormStringField extends FormFieldBase {
17
26
  type: "text" | "textarea";
package/dist/index.d.ts CHANGED
@@ -12,6 +12,15 @@ interface FormFieldBase {
12
12
  label: string;
13
13
  required?: boolean;
14
14
  placeholder?: string;
15
+ /**
16
+ * The answer this field is expected to carry.
17
+ *
18
+ * Declaring it lets the form report how the submission compared — a quiz colours itself the
19
+ * moment it is answered instead of waiting for a round trip to say so. Unlike the constraints
20
+ * beside it this never blocks a submission: a wrong answer is an answer, and the person is told
21
+ * rather than stopped.
22
+ */
23
+ expect?: string | number | boolean;
15
24
  }
16
25
  interface FormStringField extends FormFieldBase {
17
26
  type: "text" | "textarea";
package/dist/index.js CHANGED
@@ -27,7 +27,8 @@ const FIELD_KEYS = new Set([
27
27
  "min",
28
28
  "max",
29
29
  "options",
30
- "placeholder"
30
+ "placeholder",
31
+ "expect"
31
32
  ]);
32
33
  const OPTION_KEYS = new Set(["label", "value"]);
33
34
  const SAFE_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
@@ -208,12 +209,16 @@ function parseField(value, index, issues) {
208
209
  issues.push(`${path}.pattern must be a valid regular expression.`);
209
210
  }
210
211
  }
212
+ let expected;
213
+ if (value.expect !== void 0) if (typeof value.expect === "string" || typeof value.expect === "number" || typeof value.expect === "boolean") expected = value.expect;
214
+ else issues.push(`${path}.expect must be a string, a number, or a boolean.`);
211
215
  if (!name || !label || !type) return void 0;
212
216
  const base = {
213
217
  name,
214
218
  label,
215
219
  ...value.required === true ? { required: true } : {},
216
- ...placeholder === void 0 ? {} : { placeholder }
220
+ ...placeholder === void 0 ? {} : { placeholder },
221
+ ...expected === void 0 ? {} : { expect: expected }
217
222
  };
218
223
  if (type === "number") {
219
224
  if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || value.options !== void 0) issues.push(`${path} contains constraints unsupported by number fields.`);
@@ -318,13 +323,35 @@ function mountForm(host, definition, options) {
318
323
  submit.textContent = definition.submitLabel ?? "Submit";
319
324
  formElement.appendChild(submit);
320
325
  /**
326
+ * Compare the submission with the answers the fields declared.
327
+ *
328
+ * Only fields carrying `expect` take part, so a plain form is never marked. The tone is the worst
329
+ * of them: one wrong answer among three right ones is not a pass.
330
+ */
331
+ const gradeAgainstExpectations = (values) => {
332
+ const graded = definition.fields.filter((field) => field.expect !== void 0);
333
+ if (graded.length === 0) return void 0;
334
+ const fieldTones = {};
335
+ let wrong = 0;
336
+ for (const field of graded) {
337
+ const submitted$1 = values[field.name];
338
+ const matches = typeof field.expect === "string" && typeof submitted$1 === "string" ? submitted$1.trim() === field.expect.trim() : submitted$1 === field.expect;
339
+ fieldTones[field.name] = matches ? "positive" : "warning";
340
+ if (!matches) wrong += 1;
341
+ }
342
+ return {
343
+ tone: wrong === 0 ? "positive" : "warning",
344
+ fields: fieldTones
345
+ };
346
+ };
347
+ /**
321
348
  * Show how the submission turned out, when the handler judged it.
322
349
  *
323
350
  * The result used to be discarded, so an app that knew the answer was wrong had no way to say so
324
351
  * — the form simply disabled itself and read "Submitted" whether the answer was right or not.
325
352
  */
326
- const applyOutcome = (result) => {
327
- const outcome = actionOutcome(result);
353
+ const applyOutcome = (result, fallback) => {
354
+ const outcome = actionOutcome(result) ?? fallback;
328
355
  if (!outcome || disposed) return;
329
356
  formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
330
357
  if (outcome.message) {
@@ -361,6 +388,7 @@ function mountForm(host, definition, options) {
361
388
  pending = true;
362
389
  submit.disabled = true;
363
390
  formElement.setAttribute("aria-busy", "true");
391
+ const graded = gradeAgainstExpectations(validation.values);
364
392
  const settle = () => {
365
393
  if (disposed) return;
366
394
  pending = false;
@@ -376,7 +404,7 @@ function mountForm(host, definition, options) {
376
404
  signal: controller.signal
377
405
  }).then((result) => {
378
406
  markSubmitted();
379
- applyOutcome(result);
407
+ applyOutcome(result, graded);
380
408
  }, (error) => {
381
409
  if (!disposed && !controller.signal.aborted) {
382
410
  actionError.textContent = actionErrorMessage(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/plugin-form",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Safe interactive form blocks for AIGUI, backed by the core ActionRuntime.",
5
5
  "keywords": [
6
6
  "llm",
@@ -49,7 +49,7 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@ai-gui/core": "0.7.0"
52
+ "@ai-gui/core": "0.8.0"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsdown",