@ai-gui/plugin-form 0.11.1 → 0.12.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
@@ -393,7 +393,12 @@ function mountForm(host, definition, options) {
393
393
  for (const element of Array.from(formElement.elements)) if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element instanceof HTMLButtonElement || element instanceof HTMLFieldSetElement) element.disabled = true;
394
394
  submit.textContent = options.submittedLabel ?? "Submitted";
395
395
  };
396
- if (submitted) markSubmitted();
396
+ const stored = options.restore?.(definition.id);
397
+ if (stored) {
398
+ writeControls(definition, controls, stored.values);
399
+ markSubmitted();
400
+ applyOutcome(stored.outcome, gradeAgainstExpectations(stored.values));
401
+ } else if (submitted) markSubmitted();
397
402
  const onInput = (event) => {
398
403
  const control = event.target;
399
404
  if (!(control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement || control instanceof HTMLSelectElement)) return;
@@ -429,6 +434,14 @@ function mountForm(host, definition, options) {
429
434
  }).then((result) => {
430
435
  markSubmitted();
431
436
  applyOutcome(result, graded);
437
+ if (disposed) return;
438
+ const outcome = (0, __ai_gui_core.actionOutcome)(result) ?? graded;
439
+ try {
440
+ options.onSubmitted?.(definition.id, {
441
+ values: validation.values,
442
+ ...outcome ? { outcome } : {}
443
+ });
444
+ } catch {}
432
445
  }, (error) => {
433
446
  if (!disposed && !controller.signal.aborted) {
434
447
  actionError.textContent = actionErrorMessage(error);
@@ -545,6 +558,26 @@ function readControls(definition, controls) {
545
558
  }
546
559
  return values;
547
560
  }
561
+ /**
562
+ * Put a stored submission back into the controls — the inverse of `readControls`.
563
+ *
564
+ * A radio group is several inputs sharing a name, and `controls` holds only the first, so the one
565
+ * that carries the stored value has to be found in the group. Setting `.value` on that first input
566
+ * would rename the option instead of choosing it.
567
+ */
568
+ function writeControls(definition, controls, values) {
569
+ for (const field of definition.fields) {
570
+ const control = controls.get(field.name);
571
+ const value = values[field.name];
572
+ if (!control || value === void 0) continue;
573
+ if (field.type === "checkbox" && control instanceof HTMLInputElement) control.checked = value === true;
574
+ else if (field.type === "radio") {
575
+ const group = control.form?.elements.namedItem(field.name);
576
+ const inputs = group instanceof RadioNodeList ? Array.from(group) : [control];
577
+ for (const input of inputs) if (input instanceof HTMLInputElement) input.checked = input.value === String(value);
578
+ } else control.value = String(value);
579
+ }
580
+ }
548
581
  function renderErrors(errors, controls, elements) {
549
582
  for (const name of controls.keys()) {
550
583
  const message = errors[name];
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AIGuiPlugin, ActionRuntime } from "@ai-gui/core";
1
+ import { AIGuiPlugin, ActionOutcome, ActionRuntime } from "@ai-gui/core";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  type FormFieldType = "text" | "textarea" | "number" | "date" | "select" | "checkbox" | "radio";
@@ -62,16 +62,43 @@ interface FormValidationResult {
62
62
  values: Record<string, string | number | boolean>;
63
63
  errors: Record<string, string>;
64
64
  }
65
+ /** What was answered, and how it turned out — enough to put a form back the way it was left. */
66
+ interface FormSubmission {
67
+ values: Record<string, string | number | boolean>;
68
+ /**
69
+ * The verdict, when the host kept one. Omitted, the fields' own `expect` is graded again, so a
70
+ * quiz still comes back coloured without the host having to store the marking.
71
+ */
72
+ outcome?: ActionOutcome;
73
+ }
65
74
  interface FormPluginOptions {
66
75
  /** Shared core runtime whose registry is the only allowlist for submitAction. */
67
76
  actionRuntime: ActionRuntime;
68
- /** Mount forms as already submitted, useful when restoring persisted conversations. */
77
+ /** Mount every form as already submitted, useful when restoring persisted conversations. */
69
78
  submitted?: boolean;
70
79
  /** Label shown after a successful or restored submission. */
71
80
  submittedLabel?: string;
81
+ /**
82
+ * The submission this form already has, by form id.
83
+ *
84
+ * `submitted` on its own marks a form done without saying what was answered, so a reloaded
85
+ * conversation shows a disabled question with nothing chosen in it — which reads as worse than
86
+ * unanswered, since it claims to have been answered and cannot say with what. Returning the
87
+ * values puts the person's own answer back in front of them.
88
+ *
89
+ * Called once per form as it mounts, so it can read whatever the host has loaded by then.
90
+ */
91
+ restore?: (formId: string) => FormSubmission | undefined;
92
+ /**
93
+ * Called when a submission succeeds, so the host can persist what to restore later.
94
+ *
95
+ * The action handler sees the values too, but not which form they came from — the form id is only
96
+ * in `cardType`, which a host would have to parse. This hands it over directly.
97
+ */
98
+ onSubmitted?: (formId: string, submission: FormSubmission) => void;
72
99
  }
73
100
  declare function formPromptSpec(): string;
74
101
  declare function form(options: FormPluginOptions): AIGuiPlugin;
75
102
  declare function parseFormDefinition(source: string): FormParseResult;
76
103
  declare function validateFormValues(definition: FormDefinition, input: Record<string, unknown>): FormValidationResult; //#endregion
77
- export { FormCheckboxField, FormChoiceField, FormDateField, FormDefinition, FormField, FormFieldType, FormNumberField, FormOption, FormParseResult, FormPluginOptions, FormStringField, FormValidationResult, form, formPromptSpec, parseFormDefinition, validateFormValues };
104
+ export { FormCheckboxField, FormChoiceField, FormDateField, FormDefinition, FormField, FormFieldType, FormNumberField, FormOption, FormParseResult, FormPluginOptions, FormStringField, FormSubmission, FormValidationResult, form, formPromptSpec, parseFormDefinition, validateFormValues };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AIGuiPlugin, ActionRuntime } from "@ai-gui/core";
1
+ import { AIGuiPlugin, ActionOutcome, ActionRuntime } from "@ai-gui/core";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  type FormFieldType = "text" | "textarea" | "number" | "date" | "select" | "checkbox" | "radio";
@@ -62,16 +62,43 @@ interface FormValidationResult {
62
62
  values: Record<string, string | number | boolean>;
63
63
  errors: Record<string, string>;
64
64
  }
65
+ /** What was answered, and how it turned out — enough to put a form back the way it was left. */
66
+ interface FormSubmission {
67
+ values: Record<string, string | number | boolean>;
68
+ /**
69
+ * The verdict, when the host kept one. Omitted, the fields' own `expect` is graded again, so a
70
+ * quiz still comes back coloured without the host having to store the marking.
71
+ */
72
+ outcome?: ActionOutcome;
73
+ }
65
74
  interface FormPluginOptions {
66
75
  /** Shared core runtime whose registry is the only allowlist for submitAction. */
67
76
  actionRuntime: ActionRuntime;
68
- /** Mount forms as already submitted, useful when restoring persisted conversations. */
77
+ /** Mount every form as already submitted, useful when restoring persisted conversations. */
69
78
  submitted?: boolean;
70
79
  /** Label shown after a successful or restored submission. */
71
80
  submittedLabel?: string;
81
+ /**
82
+ * The submission this form already has, by form id.
83
+ *
84
+ * `submitted` on its own marks a form done without saying what was answered, so a reloaded
85
+ * conversation shows a disabled question with nothing chosen in it — which reads as worse than
86
+ * unanswered, since it claims to have been answered and cannot say with what. Returning the
87
+ * values puts the person's own answer back in front of them.
88
+ *
89
+ * Called once per form as it mounts, so it can read whatever the host has loaded by then.
90
+ */
91
+ restore?: (formId: string) => FormSubmission | undefined;
92
+ /**
93
+ * Called when a submission succeeds, so the host can persist what to restore later.
94
+ *
95
+ * The action handler sees the values too, but not which form they came from — the form id is only
96
+ * in `cardType`, which a host would have to parse. This hands it over directly.
97
+ */
98
+ onSubmitted?: (formId: string, submission: FormSubmission) => void;
72
99
  }
73
100
  declare function formPromptSpec(): string;
74
101
  declare function form(options: FormPluginOptions): AIGuiPlugin;
75
102
  declare function parseFormDefinition(source: string): FormParseResult;
76
103
  declare function validateFormValues(definition: FormDefinition, input: Record<string, unknown>): FormValidationResult; //#endregion
77
- export { FormCheckboxField, FormChoiceField, FormDateField, FormDefinition, FormField, FormFieldType, FormNumberField, FormOption, FormParseResult, FormPluginOptions, FormStringField, FormValidationResult, form, formPromptSpec, parseFormDefinition, validateFormValues };
104
+ export { FormCheckboxField, FormChoiceField, FormDateField, FormDefinition, FormField, FormFieldType, FormNumberField, FormOption, FormParseResult, FormPluginOptions, FormStringField, FormSubmission, FormValidationResult, form, formPromptSpec, parseFormDefinition, validateFormValues };
package/dist/index.js CHANGED
@@ -369,7 +369,12 @@ function mountForm(host, definition, options) {
369
369
  for (const element of Array.from(formElement.elements)) if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element instanceof HTMLButtonElement || element instanceof HTMLFieldSetElement) element.disabled = true;
370
370
  submit.textContent = options.submittedLabel ?? "Submitted";
371
371
  };
372
- if (submitted) markSubmitted();
372
+ const stored = options.restore?.(definition.id);
373
+ if (stored) {
374
+ writeControls(definition, controls, stored.values);
375
+ markSubmitted();
376
+ applyOutcome(stored.outcome, gradeAgainstExpectations(stored.values));
377
+ } else if (submitted) markSubmitted();
373
378
  const onInput = (event) => {
374
379
  const control = event.target;
375
380
  if (!(control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement || control instanceof HTMLSelectElement)) return;
@@ -405,6 +410,14 @@ function mountForm(host, definition, options) {
405
410
  }).then((result) => {
406
411
  markSubmitted();
407
412
  applyOutcome(result, graded);
413
+ if (disposed) return;
414
+ const outcome = actionOutcome(result) ?? graded;
415
+ try {
416
+ options.onSubmitted?.(definition.id, {
417
+ values: validation.values,
418
+ ...outcome ? { outcome } : {}
419
+ });
420
+ } catch {}
408
421
  }, (error) => {
409
422
  if (!disposed && !controller.signal.aborted) {
410
423
  actionError.textContent = actionErrorMessage(error);
@@ -521,6 +534,26 @@ function readControls(definition, controls) {
521
534
  }
522
535
  return values;
523
536
  }
537
+ /**
538
+ * Put a stored submission back into the controls — the inverse of `readControls`.
539
+ *
540
+ * A radio group is several inputs sharing a name, and `controls` holds only the first, so the one
541
+ * that carries the stored value has to be found in the group. Setting `.value` on that first input
542
+ * would rename the option instead of choosing it.
543
+ */
544
+ function writeControls(definition, controls, values) {
545
+ for (const field of definition.fields) {
546
+ const control = controls.get(field.name);
547
+ const value = values[field.name];
548
+ if (!control || value === void 0) continue;
549
+ if (field.type === "checkbox" && control instanceof HTMLInputElement) control.checked = value === true;
550
+ else if (field.type === "radio") {
551
+ const group = control.form?.elements.namedItem(field.name);
552
+ const inputs = group instanceof RadioNodeList ? Array.from(group) : [control];
553
+ for (const input of inputs) if (input instanceof HTMLInputElement) input.checked = input.value === String(value);
554
+ } else control.value = String(value);
555
+ }
556
+ }
524
557
  function renderErrors(errors, controls, elements) {
525
558
  for (const name of controls.keys()) {
526
559
  const message = errors[name];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/plugin-form",
3
- "version": "0.11.1",
3
+ "version": "0.12.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.11.1"
52
+ "@ai-gui/core": "0.12.0"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsdown",