@ai-gui/plugin-form 0.6.2 → 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.`);
@@ -318,22 +323,67 @@ function mountForm(host, definition, options) {
318
323
  formElement.setAttribute("data-aigui-form-instance", instanceId);
319
324
  const controls = new Map();
320
325
  const errorElements = new Map();
326
+ const fieldContainers = new Map();
321
327
  for (const field of definition.fields) {
322
328
  const rendered = createField(instancePrefix, field);
323
329
  formElement.appendChild(rendered.container);
324
330
  controls.set(field.name, rendered.control);
325
331
  errorElements.set(field.name, rendered.error);
332
+ fieldContainers.set(field.name, rendered.container);
326
333
  }
327
334
  const actionError = document.createElement("div");
328
335
  actionError.setAttribute("data-aigui-form-action-error", "");
329
336
  actionError.setAttribute("role", "alert");
330
337
  actionError.hidden = true;
331
338
  formElement.appendChild(actionError);
339
+ const outcomeMessage = document.createElement("div");
340
+ outcomeMessage.setAttribute("data-aigui-form-outcome-message", "");
341
+ outcomeMessage.setAttribute("aria-live", "polite");
342
+ outcomeMessage.hidden = true;
343
+ formElement.appendChild(outcomeMessage);
332
344
  const submit = document.createElement("button");
333
345
  submit.type = "button";
334
346
  submit.setAttribute("data-aigui-form-submit", "");
335
347
  submit.textContent = definition.submitLabel ?? "Submit";
336
348
  formElement.appendChild(submit);
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
+ /**
372
+ * Show how the submission turned out, when the handler judged it.
373
+ *
374
+ * The result used to be discarded, so an app that knew the answer was wrong had no way to say so
375
+ * — the form simply disabled itself and read "Submitted" whether the answer was right or not.
376
+ */
377
+ const applyOutcome = (result, fallback) => {
378
+ const outcome = (0, __ai_gui_core.actionOutcome)(result) ?? fallback;
379
+ if (!outcome || disposed) return;
380
+ formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
381
+ if (outcome.message) {
382
+ outcomeMessage.textContent = outcome.message;
383
+ outcomeMessage.hidden = false;
384
+ }
385
+ for (const [name, tone] of Object.entries(outcome.fields ?? {})) fieldContainers.get(name)?.setAttribute("data-aigui-form-field-outcome", tone);
386
+ };
337
387
  const markSubmitted = () => {
338
388
  if (disposed) return;
339
389
  pending = false;
@@ -362,6 +412,7 @@ function mountForm(host, definition, options) {
362
412
  pending = true;
363
413
  submit.disabled = true;
364
414
  formElement.setAttribute("aria-busy", "true");
415
+ const graded = gradeAgainstExpectations(validation.values);
365
416
  const settle = () => {
366
417
  if (disposed) return;
367
418
  pending = false;
@@ -375,7 +426,10 @@ function mountForm(host, definition, options) {
375
426
  }, {
376
427
  owner,
377
428
  signal: controller.signal
378
- }).then(markSubmitted, (error) => {
429
+ }).then((result) => {
430
+ markSubmitted();
431
+ applyOutcome(result, graded);
432
+ }, (error) => {
379
433
  if (!disposed && !controller.signal.aborted) {
380
434
  actionError.textContent = actionErrorMessage(error);
381
435
  actionError.hidden = false;
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
@@ -1,4 +1,4 @@
1
- import { ActionRuntimeError } from "@ai-gui/core";
1
+ import { ActionRuntimeError, actionOutcome } from "@ai-gui/core";
2
2
 
3
3
  //#region src/index.ts
4
4
  const FIELD_TYPES = new Set([
@@ -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.`);
@@ -294,22 +299,67 @@ function mountForm(host, definition, options) {
294
299
  formElement.setAttribute("data-aigui-form-instance", instanceId);
295
300
  const controls = new Map();
296
301
  const errorElements = new Map();
302
+ const fieldContainers = new Map();
297
303
  for (const field of definition.fields) {
298
304
  const rendered = createField(instancePrefix, field);
299
305
  formElement.appendChild(rendered.container);
300
306
  controls.set(field.name, rendered.control);
301
307
  errorElements.set(field.name, rendered.error);
308
+ fieldContainers.set(field.name, rendered.container);
302
309
  }
303
310
  const actionError = document.createElement("div");
304
311
  actionError.setAttribute("data-aigui-form-action-error", "");
305
312
  actionError.setAttribute("role", "alert");
306
313
  actionError.hidden = true;
307
314
  formElement.appendChild(actionError);
315
+ const outcomeMessage = document.createElement("div");
316
+ outcomeMessage.setAttribute("data-aigui-form-outcome-message", "");
317
+ outcomeMessage.setAttribute("aria-live", "polite");
318
+ outcomeMessage.hidden = true;
319
+ formElement.appendChild(outcomeMessage);
308
320
  const submit = document.createElement("button");
309
321
  submit.type = "button";
310
322
  submit.setAttribute("data-aigui-form-submit", "");
311
323
  submit.textContent = definition.submitLabel ?? "Submit";
312
324
  formElement.appendChild(submit);
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
+ /**
348
+ * Show how the submission turned out, when the handler judged it.
349
+ *
350
+ * The result used to be discarded, so an app that knew the answer was wrong had no way to say so
351
+ * — the form simply disabled itself and read "Submitted" whether the answer was right or not.
352
+ */
353
+ const applyOutcome = (result, fallback) => {
354
+ const outcome = actionOutcome(result) ?? fallback;
355
+ if (!outcome || disposed) return;
356
+ formElement.setAttribute("data-aigui-form-outcome", outcome.tone);
357
+ if (outcome.message) {
358
+ outcomeMessage.textContent = outcome.message;
359
+ outcomeMessage.hidden = false;
360
+ }
361
+ for (const [name, tone] of Object.entries(outcome.fields ?? {})) fieldContainers.get(name)?.setAttribute("data-aigui-form-field-outcome", tone);
362
+ };
313
363
  const markSubmitted = () => {
314
364
  if (disposed) return;
315
365
  pending = false;
@@ -338,6 +388,7 @@ function mountForm(host, definition, options) {
338
388
  pending = true;
339
389
  submit.disabled = true;
340
390
  formElement.setAttribute("aria-busy", "true");
391
+ const graded = gradeAgainstExpectations(validation.values);
341
392
  const settle = () => {
342
393
  if (disposed) return;
343
394
  pending = false;
@@ -351,7 +402,10 @@ function mountForm(host, definition, options) {
351
402
  }, {
352
403
  owner,
353
404
  signal: controller.signal
354
- }).then(markSubmitted, (error) => {
405
+ }).then((result) => {
406
+ markSubmitted();
407
+ applyOutcome(result, graded);
408
+ }, (error) => {
355
409
  if (!disposed && !controller.signal.aborted) {
356
410
  actionError.textContent = actionErrorMessage(error);
357
411
  actionError.hidden = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/plugin-form",
3
- "version": "0.6.2",
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.6.2"
52
+ "@ai-gui/core": "0.8.0"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsdown",