@ai-gui/plugin-form 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Liang Li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @ai-gui/plugin-form
2
+
3
+ Safe, framework-neutral interactive forms for AIGUI. React, Vue, and vanilla adapters all host the same DOM mount output.
4
+
5
+ ```ts
6
+ import { ActionRegistry, createActionRuntime } from "@ai-gui/core"
7
+ import { form } from "@ai-gui/plugin-form"
8
+
9
+ const actions = new ActionRegistry()
10
+ actions.register({
11
+ type: "travel.search",
12
+ schema: {
13
+ type: "object",
14
+ required: ["from"],
15
+ properties: { from: { type: "string" } },
16
+ additionalProperties: false,
17
+ },
18
+ run: (params, { signal }) => searchTravel(params, signal),
19
+ })
20
+
21
+ const plugins = [form({ actionRuntime: createActionRuntime({ registry: actions }) })]
22
+ ```
23
+
24
+ The model may emit:
25
+
26
+ ````md
27
+ ```form
28
+ {
29
+ "id": "travel-search",
30
+ "fields": [
31
+ { "name": "from", "type": "text", "label": "From", "required": true },
32
+ { "name": "date", "type": "date", "label": "Departure" }
33
+ ],
34
+ "submitAction": "travel.search",
35
+ "submitLabel": "Search"
36
+ }
37
+ ```
38
+ ````
39
+
40
+ Supported fields are `text`, `textarea`, `number`, `date`, `select`, `checkbox`, and `radio`. Supported constraints are `required`, `minLength`, `maxLength`, `pattern`, `min`, and `max`.
41
+
42
+ `submitAction` must already exist in the injected `ActionRuntime`. Form JSON rejects unknown properties, HTML, URLs, scripts, event handlers, and dynamic component names. Each mounted form owns its pending state and cancellation scope; adapter teardown aborts only that form's request.
package/dist/index.cjs ADDED
@@ -0,0 +1,625 @@
1
+ "use strict";
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ const __ai_gui_core = __toESM(require("@ai-gui/core"));
26
+
27
+ //#region src/index.ts
28
+ const FIELD_TYPES = new Set([
29
+ "text",
30
+ "textarea",
31
+ "number",
32
+ "date",
33
+ "select",
34
+ "checkbox",
35
+ "radio"
36
+ ]);
37
+ const FORM_KEYS = new Set([
38
+ "id",
39
+ "fields",
40
+ "submitAction",
41
+ "submitLabel"
42
+ ]);
43
+ const FIELD_KEYS = new Set([
44
+ "name",
45
+ "type",
46
+ "label",
47
+ "required",
48
+ "minLength",
49
+ "maxLength",
50
+ "pattern",
51
+ "min",
52
+ "max",
53
+ "options",
54
+ "placeholder"
55
+ ]);
56
+ const OPTION_KEYS = new Set(["label", "value"]);
57
+ const SAFE_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
58
+ const SAFE_ID = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
59
+ const MAX_SOURCE_LENGTH = 64 * 1024;
60
+ const MAX_FIELDS = 100;
61
+ const MAX_PATTERN_LENGTH = 128;
62
+ let nextFormInstanceId = 0;
63
+ function formPromptSpec() {
64
+ return [
65
+ "Forms (fenced): ```form <safe form JSON>```.",
66
+ "Fields: text, textarea, number, date, select, checkbox, radio. Constraints: required, minLength, maxLength, pattern, min, max.",
67
+ "submitAction must name an application-registered Action. Never emit URLs, scripts, HTML, handlers, or component names."
68
+ ].join("\n");
69
+ }
70
+ function form(options) {
71
+ if (!options?.actionRuntime) throw new TypeError("form() requires an actionRuntime");
72
+ const outputs = new WeakMap();
73
+ const render = (node) => {
74
+ const cached = outputs.get(node);
75
+ if (cached) return cached;
76
+ if (!node.complete) {
77
+ const output$1 = {
78
+ kind: "html",
79
+ html: "<div data-aigui-block-loading data-block-type=\"form\"></div>"
80
+ };
81
+ outputs.set(node, output$1);
82
+ return output$1;
83
+ }
84
+ const parsed = parseFormDefinition(node.content ?? "");
85
+ if (!parsed.valid) {
86
+ const output$1 = invalidOutput(parsed.issues);
87
+ outputs.set(node, output$1);
88
+ return output$1;
89
+ }
90
+ if (!options.actionRuntime.hasAction(parsed.data.submitAction)) {
91
+ const output$1 = invalidOutput([`Action "${parsed.data.submitAction}" is not registered.`]);
92
+ outputs.set(node, output$1);
93
+ return output$1;
94
+ }
95
+ const output = {
96
+ kind: "mount",
97
+ mount: (host) => mountForm(host, parsed.data, options.actionRuntime)
98
+ };
99
+ outputs.set(node, output);
100
+ return output;
101
+ };
102
+ return {
103
+ name: "form",
104
+ nodeRenderers: { form: render },
105
+ promptSpec: formPromptSpec()
106
+ };
107
+ }
108
+ function parseFormDefinition(source) {
109
+ if (source.length > MAX_SOURCE_LENGTH) return {
110
+ valid: false,
111
+ issues: ["Form definition is too large."]
112
+ };
113
+ let value;
114
+ try {
115
+ value = JSON.parse(source);
116
+ } catch {
117
+ return {
118
+ valid: false,
119
+ issues: ["Form definition must be valid JSON."]
120
+ };
121
+ }
122
+ if (!isPlainObject(value)) return {
123
+ valid: false,
124
+ issues: ["Form definition must be an object."]
125
+ };
126
+ const issues = [];
127
+ rejectUnknownKeys(value, FORM_KEYS, "$", issues);
128
+ const id = readString(value.id, "$.id", issues, 128);
129
+ if (id && !SAFE_ID.test(id)) issues.push("$.id must start with a letter and contain only letters, numbers, underscores, or hyphens.");
130
+ const submitAction = readString(value.submitAction, "$.submitAction", issues, 256);
131
+ const submitLabel = value.submitLabel === void 0 ? void 0 : readString(value.submitLabel, "$.submitLabel", issues, 128);
132
+ if (!Array.isArray(value.fields)) issues.push("$.fields must be an array.");
133
+ else if (value.fields.length > MAX_FIELDS) issues.push(`$.fields must contain at most ${MAX_FIELDS} fields.`);
134
+ const fields = [];
135
+ const names = new Set();
136
+ if (Array.isArray(value.fields)) value.fields.forEach((candidate, index) => {
137
+ const field = parseField(candidate, index, issues);
138
+ if (!field) return;
139
+ if (names.has(field.name)) issues.push(`$.fields[${index}].name must be unique.`);
140
+ names.add(field.name);
141
+ fields.push(field);
142
+ });
143
+ if (issues.length > 0 || !id || !submitAction) return {
144
+ valid: false,
145
+ issues
146
+ };
147
+ return {
148
+ valid: true,
149
+ data: {
150
+ id,
151
+ fields,
152
+ submitAction,
153
+ ...submitLabel === void 0 ? {} : { submitLabel }
154
+ }
155
+ };
156
+ }
157
+ function validateFormValues(definition, input) {
158
+ const values = Object.create(null);
159
+ const errors = Object.create(null);
160
+ for (const field of definition.fields) {
161
+ const raw = input[field.name];
162
+ if (field.type === "checkbox") {
163
+ const value$1 = raw === true;
164
+ values[field.name] = value$1;
165
+ if (field.required && !value$1) errors[field.name] = "This field is required.";
166
+ continue;
167
+ }
168
+ if (field.type === "number") {
169
+ if (raw === "" || raw === void 0 || raw === null) {
170
+ if (field.required) errors[field.name] = "This field is required.";
171
+ continue;
172
+ }
173
+ const value$1 = typeof raw === "number" ? raw : Number(raw);
174
+ if (!Number.isFinite(value$1)) {
175
+ errors[field.name] = "Must be a number.";
176
+ continue;
177
+ }
178
+ values[field.name] = value$1;
179
+ if (field.min !== void 0 && value$1 < field.min) errors[field.name] = `Must be at least ${field.min}.`;
180
+ else if (field.max !== void 0 && value$1 > field.max) errors[field.name] = `Must be at most ${field.max}.`;
181
+ continue;
182
+ }
183
+ const value = typeof raw === "string" ? raw : "";
184
+ if (value === "") {
185
+ if (field.required) errors[field.name] = "This field is required.";
186
+ continue;
187
+ }
188
+ values[field.name] = value;
189
+ if (field.type === "select" || field.type === "radio") {
190
+ if (!field.options.some((option) => option.value === value)) errors[field.name] = "Select an allowed option.";
191
+ continue;
192
+ }
193
+ if (field.type === "date") continue;
194
+ if ("minLength" in field && field.minLength !== void 0 && value.length < field.minLength) errors[field.name] = `Must contain at least ${field.minLength} characters.`;
195
+ else if ("maxLength" in field && field.maxLength !== void 0 && value.length > field.maxLength) errors[field.name] = `Must contain at most ${field.maxLength} characters.`;
196
+ else if ("pattern" in field && field.pattern !== void 0 && !new RegExp(field.pattern).test(value)) errors[field.name] = "Must match the required format.";
197
+ }
198
+ return {
199
+ valid: Object.keys(errors).length === 0,
200
+ values,
201
+ errors
202
+ };
203
+ }
204
+ function parseField(value, index, issues) {
205
+ const path = `$.fields[${index}]`;
206
+ if (!isPlainObject(value)) {
207
+ issues.push(`${path} must be an object.`);
208
+ return void 0;
209
+ }
210
+ rejectUnknownKeys(value, FIELD_KEYS, path, issues);
211
+ const name = readString(value.name, `${path}.name`, issues, 128);
212
+ const label = readString(value.label, `${path}.label`, issues, 256);
213
+ const rawType = readString(value.type, `${path}.type`, issues, 32);
214
+ const type = rawType && FIELD_TYPES.has(rawType) ? rawType : void 0;
215
+ if (name && !SAFE_NAME.test(name)) issues.push(`${path}.name is not a safe field name.`);
216
+ if (rawType && !type) issues.push(`${path}.type is not supported.`);
217
+ if (value.required !== void 0 && typeof value.required !== "boolean") issues.push(`${path}.required must be a boolean.`);
218
+ const placeholder = value.placeholder === void 0 ? void 0 : readString(value.placeholder, `${path}.placeholder`, issues, 256);
219
+ const minLength = readOptionalInteger(value.minLength, `${path}.minLength`, issues);
220
+ const maxLength = readOptionalInteger(value.maxLength, `${path}.maxLength`, issues);
221
+ const min = readOptionalNumber(value.min, `${path}.min`, issues);
222
+ const max = readOptionalNumber(value.max, `${path}.max`, issues);
223
+ if (minLength !== void 0 && maxLength !== void 0 && minLength > maxLength) issues.push(`${path}.minLength cannot exceed maxLength.`);
224
+ if (min !== void 0 && max !== void 0 && min > max) issues.push(`${path}.min cannot exceed max.`);
225
+ let pattern;
226
+ if (value.pattern !== void 0) {
227
+ pattern = readString(value.pattern, `${path}.pattern`, issues, MAX_PATTERN_LENGTH);
228
+ if (pattern !== void 0) if (!isSafePattern(pattern)) issues.push(`${path}.pattern uses unsupported regular expression features.`);
229
+ else try {
230
+ new RegExp(pattern);
231
+ } catch {
232
+ issues.push(`${path}.pattern must be a valid regular expression.`);
233
+ }
234
+ }
235
+ if (!name || !label || !type) return void 0;
236
+ const base = {
237
+ name,
238
+ label,
239
+ ...value.required === true ? { required: true } : {},
240
+ ...placeholder === void 0 ? {} : { placeholder }
241
+ };
242
+ if (type === "number") {
243
+ if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || value.options !== void 0) issues.push(`${path} contains constraints unsupported by number fields.`);
244
+ return {
245
+ ...base,
246
+ type,
247
+ ...min === void 0 ? {} : { min },
248
+ ...max === void 0 ? {} : { max }
249
+ };
250
+ }
251
+ if (type === "select" || type === "radio") {
252
+ if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || min !== void 0 || max !== void 0 || placeholder !== void 0) issues.push(`${path} contains unsupported choice-field properties.`);
253
+ const options = parseOptions(value.options, `${path}.options`, issues);
254
+ return {
255
+ ...base,
256
+ type,
257
+ options
258
+ };
259
+ }
260
+ if (type === "checkbox") {
261
+ if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || min !== void 0 || max !== void 0 || value.options !== void 0 || placeholder !== void 0) issues.push(`${path} contains unsupported checkbox properties.`);
262
+ return {
263
+ ...base,
264
+ type
265
+ };
266
+ }
267
+ if (type === "date") {
268
+ if (minLength !== void 0 || maxLength !== void 0 || pattern !== void 0 || min !== void 0 || max !== void 0 || value.options !== void 0 || placeholder !== void 0) issues.push(`${path} contains unsupported date-field properties.`);
269
+ return {
270
+ ...base,
271
+ type
272
+ };
273
+ }
274
+ if (min !== void 0 || max !== void 0 || value.options !== void 0) issues.push(`${path} contains unsupported string-field properties.`);
275
+ return {
276
+ ...base,
277
+ type,
278
+ ...minLength === void 0 ? {} : { minLength },
279
+ ...maxLength === void 0 ? {} : { maxLength },
280
+ ...pattern === void 0 ? {} : { pattern }
281
+ };
282
+ }
283
+ function parseOptions(value, path, issues) {
284
+ if (!Array.isArray(value) || value.length === 0 || value.length > 100) {
285
+ issues.push(`${path} must be a non-empty array with at most 100 options.`);
286
+ return [];
287
+ }
288
+ const seen = new Set();
289
+ return value.flatMap((option, index) => {
290
+ const itemPath = `${path}[${index}]`;
291
+ if (!isPlainObject(option)) {
292
+ issues.push(`${itemPath} must be an object.`);
293
+ return [];
294
+ }
295
+ rejectUnknownKeys(option, OPTION_KEYS, itemPath, issues);
296
+ const label = readString(option.label, `${itemPath}.label`, issues, 256);
297
+ const optionValue = readString(option.value, `${itemPath}.value`, issues, 256);
298
+ if (optionValue !== void 0 && seen.has(optionValue)) issues.push(`${itemPath}.value must be unique.`);
299
+ if (optionValue !== void 0) seen.add(optionValue);
300
+ return label !== void 0 && optionValue !== void 0 ? [{
301
+ label,
302
+ value: optionValue
303
+ }] : [];
304
+ });
305
+ }
306
+ function mountForm(host, definition, runtime) {
307
+ const instanceId = String(++nextFormInstanceId);
308
+ const instancePrefix = `aigui-form-${instanceId}-${definition.id}`;
309
+ const cardType = `form:${definition.id}:${instanceId}`;
310
+ const owner = {};
311
+ const controller = new AbortController();
312
+ let pending = false;
313
+ let disposed = false;
314
+ const formElement = document.createElement("form");
315
+ formElement.noValidate = true;
316
+ formElement.setAttribute("data-aigui-form", definition.id);
317
+ formElement.setAttribute("data-aigui-form-instance", instanceId);
318
+ const controls = new Map();
319
+ const errorElements = new Map();
320
+ for (const field of definition.fields) {
321
+ const rendered = createField(instancePrefix, field);
322
+ formElement.appendChild(rendered.container);
323
+ controls.set(field.name, rendered.control);
324
+ errorElements.set(field.name, rendered.error);
325
+ }
326
+ const actionError = document.createElement("div");
327
+ actionError.setAttribute("data-aigui-form-action-error", "");
328
+ actionError.setAttribute("role", "alert");
329
+ actionError.hidden = true;
330
+ formElement.appendChild(actionError);
331
+ const submit = document.createElement("button");
332
+ submit.type = "submit";
333
+ submit.textContent = definition.submitLabel ?? "Submit";
334
+ formElement.appendChild(submit);
335
+ const onInput = (event) => {
336
+ const control = event.target;
337
+ if (!(control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement || control instanceof HTMLSelectElement)) return;
338
+ clearFieldError(control.name, controls, errorElements);
339
+ actionError.hidden = true;
340
+ };
341
+ const onSubmit = (event) => {
342
+ event.preventDefault();
343
+ if (pending || disposed) return;
344
+ const validation = validateFormValues(definition, readControls(definition, controls));
345
+ renderErrors(validation.errors, controls, errorElements);
346
+ actionError.hidden = true;
347
+ if (!validation.valid) {
348
+ controls.get(Object.keys(validation.errors)[0])?.focus();
349
+ return;
350
+ }
351
+ pending = true;
352
+ submit.disabled = true;
353
+ formElement.setAttribute("aria-busy", "true");
354
+ const settle = () => {
355
+ if (disposed) return;
356
+ pending = false;
357
+ submit.disabled = false;
358
+ formElement.removeAttribute("aria-busy");
359
+ };
360
+ runtime.dispatch({
361
+ type: definition.submitAction,
362
+ params: validation.values,
363
+ cardType
364
+ }, {
365
+ owner,
366
+ signal: controller.signal
367
+ }).then(settle, (error) => {
368
+ if (!disposed && !controller.signal.aborted) {
369
+ actionError.textContent = actionErrorMessage(error);
370
+ actionError.hidden = false;
371
+ }
372
+ settle();
373
+ });
374
+ };
375
+ formElement.addEventListener("input", onInput);
376
+ formElement.addEventListener("change", onInput);
377
+ formElement.addEventListener("submit", onSubmit);
378
+ host.replaceChildren(formElement);
379
+ return () => {
380
+ disposed = true;
381
+ controller.abort();
382
+ formElement.removeEventListener("input", onInput);
383
+ formElement.removeEventListener("change", onInput);
384
+ formElement.removeEventListener("submit", onSubmit);
385
+ };
386
+ }
387
+ function createField(formId, field) {
388
+ const container = document.createElement(field.type === "radio" ? "fieldset" : "div");
389
+ container.setAttribute("data-aigui-form-field", field.name);
390
+ const controlId = `${formId}-${field.name.replace(/[^A-Za-z0-9_-]/g, "-")}`;
391
+ const errorId = `${controlId}-error`;
392
+ const error = document.createElement("div");
393
+ error.id = errorId;
394
+ error.setAttribute("data-aigui-form-field-error", field.name);
395
+ error.setAttribute("aria-live", "polite");
396
+ error.hidden = true;
397
+ if (field.type === "radio") {
398
+ const legend = document.createElement("legend");
399
+ legend.textContent = field.label;
400
+ container.appendChild(legend);
401
+ let first;
402
+ for (const [index, option] of field.options.entries()) {
403
+ const optionId = `${controlId}-${index}`;
404
+ const input = document.createElement("input");
405
+ input.type = "radio";
406
+ input.id = optionId;
407
+ input.name = field.name;
408
+ input.value = option.value;
409
+ input.required = field.required === true;
410
+ input.setAttribute("aria-describedby", errorId);
411
+ const label$1 = document.createElement("label");
412
+ label$1.htmlFor = optionId;
413
+ label$1.textContent = option.label;
414
+ container.append(input, label$1);
415
+ first ??= input;
416
+ }
417
+ container.appendChild(error);
418
+ return {
419
+ container,
420
+ control: first,
421
+ error
422
+ };
423
+ }
424
+ let control;
425
+ if (field.type === "textarea") control = document.createElement("textarea");
426
+ else if (field.type === "select") {
427
+ const select = document.createElement("select");
428
+ const empty = document.createElement("option");
429
+ empty.value = "";
430
+ empty.textContent = "Select...";
431
+ select.appendChild(empty);
432
+ for (const option of field.options) {
433
+ const element = document.createElement("option");
434
+ element.value = option.value;
435
+ element.textContent = option.label;
436
+ select.appendChild(element);
437
+ }
438
+ control = select;
439
+ } else {
440
+ const input = document.createElement("input");
441
+ input.type = field.type;
442
+ control = input;
443
+ }
444
+ control.id = controlId;
445
+ control.setAttribute("name", field.name);
446
+ control.required = field.required === true;
447
+ control.setAttribute("aria-describedby", errorId);
448
+ if (field.placeholder !== void 0 && !(control instanceof HTMLSelectElement)) control.placeholder = field.placeholder;
449
+ if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && (field.type === "text" || field.type === "textarea") && field.minLength !== void 0) control.minLength = field.minLength;
450
+ if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && (field.type === "text" || field.type === "textarea") && field.maxLength !== void 0) control.maxLength = field.maxLength;
451
+ if (field.type === "text" && field.pattern !== void 0 && control instanceof HTMLInputElement) control.pattern = field.pattern;
452
+ if (field.type === "number" && control instanceof HTMLInputElement) {
453
+ if (field.min !== void 0) control.min = String(field.min);
454
+ if (field.max !== void 0) control.max = String(field.max);
455
+ }
456
+ const label = document.createElement("label");
457
+ label.htmlFor = controlId;
458
+ label.textContent = field.label;
459
+ if (field.type === "checkbox") container.append(control, label, error);
460
+ else container.append(label, control, error);
461
+ return {
462
+ container,
463
+ control,
464
+ error
465
+ };
466
+ }
467
+ function readControls(definition, controls) {
468
+ const values = Object.create(null);
469
+ for (const field of definition.fields) {
470
+ const control = controls.get(field.name);
471
+ if (!control) continue;
472
+ if (field.type === "checkbox" && control instanceof HTMLInputElement) values[field.name] = control.checked;
473
+ else if (field.type === "radio") {
474
+ const group = control.form?.elements.namedItem(field.name);
475
+ values[field.name] = group instanceof RadioNodeList ? group.value : control instanceof HTMLInputElement && control.checked ? control.value : "";
476
+ } else values[field.name] = control.value;
477
+ }
478
+ return values;
479
+ }
480
+ function renderErrors(errors, controls, elements) {
481
+ for (const name of controls.keys()) {
482
+ const message = errors[name];
483
+ const control = controls.get(name);
484
+ const element = elements.get(name);
485
+ if (!control || !element) continue;
486
+ if (message) {
487
+ control.setAttribute("aria-invalid", "true");
488
+ element.textContent = message;
489
+ element.hidden = false;
490
+ } else clearFieldError(name, controls, elements);
491
+ }
492
+ }
493
+ function clearFieldError(name, controls, elements) {
494
+ controls.get(name)?.removeAttribute("aria-invalid");
495
+ const element = elements.get(name);
496
+ if (element) {
497
+ element.textContent = "";
498
+ element.hidden = true;
499
+ }
500
+ }
501
+ function invalidOutput(issues) {
502
+ return {
503
+ kind: "html",
504
+ html: `<div data-aigui-form-invalid="" role="alert">${escapeHtml(issues[0] ?? "Invalid form definition.")}</div>`
505
+ };
506
+ }
507
+ function actionErrorMessage(error) {
508
+ return error instanceof __ai_gui_core.ActionRuntimeError ? error.message : "The action failed.";
509
+ }
510
+ function rejectUnknownKeys(value, allowed, path, issues) {
511
+ for (const key of Object.keys(value)) if (!allowed.has(key)) issues.push(`${path}.${key} is not allowed.`);
512
+ }
513
+ function readString(value, path, issues, maxLength, allowEmpty = false) {
514
+ if (typeof value !== "string" || !allowEmpty && value.trim() === "" || value.length > maxLength) {
515
+ issues.push(`${path} must be ${allowEmpty ? "a" : "a non-empty"} string of at most ${maxLength} characters.`);
516
+ return void 0;
517
+ }
518
+ return value;
519
+ }
520
+ function readOptionalInteger(value, path, issues) {
521
+ if (value === void 0) return void 0;
522
+ if (!Number.isInteger(value) || value < 0 || value > 1e6) {
523
+ issues.push(`${path} must be a non-negative integer.`);
524
+ return void 0;
525
+ }
526
+ return value;
527
+ }
528
+ function readOptionalNumber(value, path, issues) {
529
+ if (value === void 0) return void 0;
530
+ if (typeof value !== "number" || !Number.isFinite(value)) {
531
+ issues.push(`${path} must be a finite number.`);
532
+ return void 0;
533
+ }
534
+ return value;
535
+ }
536
+ function isPlainObject(value) {
537
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
538
+ const prototype = Object.getPrototypeOf(value);
539
+ return prototype === Object.prototype || prototype === null;
540
+ }
541
+ function escapeHtml(value) {
542
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
543
+ }
544
+ function isSafePattern(pattern) {
545
+ let index = 0;
546
+ let hasAtom = false;
547
+ let canQuantify = false;
548
+ let hasQuantifier = false;
549
+ if (pattern[index] === "^") index++;
550
+ while (index < pattern.length) {
551
+ const char = pattern[index];
552
+ if (char === "$" && index === pattern.length - 1) return hasAtom;
553
+ if (char === "[") {
554
+ const end = readCharacterClass(pattern, index);
555
+ if (end === -1) return false;
556
+ index = end;
557
+ hasAtom = true;
558
+ canQuantify = true;
559
+ continue;
560
+ }
561
+ if (char === "\\") {
562
+ if (!isSafeEscape(pattern[index + 1])) return false;
563
+ index += 2;
564
+ hasAtom = true;
565
+ canQuantify = true;
566
+ continue;
567
+ }
568
+ if (char === "*" || char === "+" || char === "?") {
569
+ if (!canQuantify || hasQuantifier) return false;
570
+ index++;
571
+ canQuantify = false;
572
+ hasQuantifier = true;
573
+ continue;
574
+ }
575
+ if (char === "{") {
576
+ if (!canQuantify || hasQuantifier) return false;
577
+ const end = readBoundedQuantifier(pattern, index);
578
+ if (end === -1) return false;
579
+ index = end;
580
+ canQuantify = false;
581
+ hasQuantifier = true;
582
+ continue;
583
+ }
584
+ if (".^$|()]}".includes(char)) return false;
585
+ index++;
586
+ hasAtom = true;
587
+ canQuantify = true;
588
+ }
589
+ return hasAtom;
590
+ }
591
+ function readCharacterClass(pattern, start) {
592
+ let index = start + 1;
593
+ if (pattern[index] === "^") index++;
594
+ let hasCharacter = false;
595
+ while (index < pattern.length) {
596
+ const char = pattern[index];
597
+ if (char === "]") return hasCharacter ? index + 1 : -1;
598
+ if (char === "\\") {
599
+ if (!isSafeEscape(pattern[index + 1])) return -1;
600
+ index += 2;
601
+ } else {
602
+ if (char === "[") return -1;
603
+ index++;
604
+ }
605
+ hasCharacter = true;
606
+ }
607
+ return -1;
608
+ }
609
+ function readBoundedQuantifier(pattern, start) {
610
+ const match = /^\{(\d{1,4})(?:,(\d{0,4}))?\}/.exec(pattern.slice(start));
611
+ if (!match) return -1;
612
+ const minimum = Number(match[1]);
613
+ const maximum = match[2] === void 0 || match[2] === "" ? minimum : Number(match[2]);
614
+ if (minimum > 1e3 || maximum > 1e3 || maximum < minimum) return -1;
615
+ return start + match[0].length;
616
+ }
617
+ function isSafeEscape(char) {
618
+ return char !== void 0 && ("dDsSwW\\.^$*+?{}[]|()-".includes(char) || !/[A-Za-z0-9]/.test(char));
619
+ }
620
+
621
+ //#endregion
622
+ exports.form = form
623
+ exports.formPromptSpec = formPromptSpec
624
+ exports.parseFormDefinition = parseFormDefinition
625
+ exports.validateFormValues = validateFormValues