@alexkroman1/aai-ui 6.7.0 → 6.7.2
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/components/_form-readiness.d.ts +56 -0
- package/dist/index.js +101 -6
- package/package.json +2 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a form's own fields exist yet.
|
|
3
|
+
*
|
|
4
|
+
* `<Form>` leans entirely on NATIVE validation — it is a real `<form>` with no
|
|
5
|
+
* `noValidate`, so a `required` field is what stops an empty submit, and the
|
|
6
|
+
* module doc next door says so. That has one hole, and it is the whole reason
|
|
7
|
+
* this exists: a field set declared REMOTELY is not in the DOM while its
|
|
8
|
+
* declaration is in flight, so there is nothing for the browser to validate and
|
|
9
|
+
* an empty submit sails through.
|
|
10
|
+
*
|
|
11
|
+
* It is not theoretical. `<WorkflowFields>` renders `null` until the workflow
|
|
12
|
+
* listing lands, so the transcription desk's first click — before the one-request
|
|
13
|
+
* lookup answered — submitted a form holding only its button. The browser was
|
|
14
|
+
* happy, the payload was `{}`, and the run was refused by the agent with
|
|
15
|
+
* `Invalid input for workflow "transcribeStream": recording: Invalid input`: a
|
|
16
|
+
* schema complaint about a field the person had not been shown, naming a workflow
|
|
17
|
+
* they did not choose by name, for a file picker that appeared a moment later.
|
|
18
|
+
*
|
|
19
|
+
* ## Readiness is DECLARED by the children, because only they know
|
|
20
|
+
*
|
|
21
|
+
* `Form` cannot ask. It renders `{children}` and reads the DOM on submit, and a
|
|
22
|
+
* pending fetch leaves no trace in the DOM at all — which is exactly the
|
|
23
|
+
* difference from `data-aai-read`, the other thing a child tells the form: that
|
|
24
|
+
* one describes an element that EXISTS. So this is a context rather than an
|
|
25
|
+
* attribute, and it carries the one fact a DOM read cannot recover.
|
|
26
|
+
*
|
|
27
|
+
* A form with no such children is ready by definition — `useFormFieldsPending`
|
|
28
|
+
* outside a provider reports nothing pending, so every hand-written form is
|
|
29
|
+
* unaffected and `Form` keeps working outside this package.
|
|
30
|
+
*/
|
|
31
|
+
/** What a child calls to say whether its own fields are ready. */
|
|
32
|
+
type Readiness = (key: string, pending: boolean) => void;
|
|
33
|
+
export declare const FormReadinessProvider: import("react").Provider<Readiness | undefined>;
|
|
34
|
+
/**
|
|
35
|
+
* Track which children are still waiting for their fields.
|
|
36
|
+
*
|
|
37
|
+
* A SET keyed by the child's own `useId`, not a counter: a child that re-renders
|
|
38
|
+
* while pending must not increment twice, and one that unmounts mid-flight must
|
|
39
|
+
* not leave the form disabled forever. Both are the ordinary lifecycle here — a
|
|
40
|
+
* page that switches workflows swaps one `<WorkflowFields>` for another.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useFormReadiness(): {
|
|
43
|
+
pending: boolean;
|
|
44
|
+
declare: Readiness;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Declare from a child that its fields are, or are no longer, still loading.
|
|
48
|
+
*
|
|
49
|
+
* Reports through an EFFECT rather than during render, because this writes to a
|
|
50
|
+
* parent's state — doing it in the render body is the "cannot update a component
|
|
51
|
+
* while rendering a different component" warning, and under StrictMode it is a
|
|
52
|
+
* double report the parent has to be idempotent about anyway. The cleanup
|
|
53
|
+
* releases the claim, so an unmounted field set never holds the form shut.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useDeclareFieldsPending(pending: boolean): void;
|
|
56
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -14,12 +14,98 @@ import { client, mountRoot, resolveContainer } from "./define-client.js";
|
|
|
14
14
|
import { useAgentState, useEvent, useToolCallStart, useToolResult } from "./hooks.js";
|
|
15
15
|
import clsx from "clsx";
|
|
16
16
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
-
import { createElement, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
17
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
18
18
|
import { errorMessage, isTerminal, isTerminal as isTerminal$1 } from "@alexkroman1/aai";
|
|
19
19
|
import { isRecord, omitUndefined, safeJsonParse as safeJsonParse$1 } from "@alexkroman1/aai/utils";
|
|
20
20
|
import { createWorkflowApiClient } from "@alexkroman1/aai/workflow-api";
|
|
21
21
|
import { createParser } from "eventsource-parser";
|
|
22
22
|
import { createEpoch } from "@alexkroman1/aai/internal";
|
|
23
|
+
//#region components/_form-readiness.ts
|
|
24
|
+
/**
|
|
25
|
+
* Whether a form's own fields exist yet.
|
|
26
|
+
*
|
|
27
|
+
* `<Form>` leans entirely on NATIVE validation — it is a real `<form>` with no
|
|
28
|
+
* `noValidate`, so a `required` field is what stops an empty submit, and the
|
|
29
|
+
* module doc next door says so. That has one hole, and it is the whole reason
|
|
30
|
+
* this exists: a field set declared REMOTELY is not in the DOM while its
|
|
31
|
+
* declaration is in flight, so there is nothing for the browser to validate and
|
|
32
|
+
* an empty submit sails through.
|
|
33
|
+
*
|
|
34
|
+
* It is not theoretical. `<WorkflowFields>` renders `null` until the workflow
|
|
35
|
+
* listing lands, so the transcription desk's first click — before the one-request
|
|
36
|
+
* lookup answered — submitted a form holding only its button. The browser was
|
|
37
|
+
* happy, the payload was `{}`, and the run was refused by the agent with
|
|
38
|
+
* `Invalid input for workflow "transcribeStream": recording: Invalid input`: a
|
|
39
|
+
* schema complaint about a field the person had not been shown, naming a workflow
|
|
40
|
+
* they did not choose by name, for a file picker that appeared a moment later.
|
|
41
|
+
*
|
|
42
|
+
* ## Readiness is DECLARED by the children, because only they know
|
|
43
|
+
*
|
|
44
|
+
* `Form` cannot ask. It renders `{children}` and reads the DOM on submit, and a
|
|
45
|
+
* pending fetch leaves no trace in the DOM at all — which is exactly the
|
|
46
|
+
* difference from `data-aai-read`, the other thing a child tells the form: that
|
|
47
|
+
* one describes an element that EXISTS. So this is a context rather than an
|
|
48
|
+
* attribute, and it carries the one fact a DOM read cannot recover.
|
|
49
|
+
*
|
|
50
|
+
* A form with no such children is ready by definition — `useFormFieldsPending`
|
|
51
|
+
* outside a provider reports nothing pending, so every hand-written form is
|
|
52
|
+
* unaffected and `Form` keeps working outside this package.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Set by `Form`, read by any field set that fetches its own declaration.
|
|
56
|
+
*
|
|
57
|
+
* `undefined` means no `Form` above, which is legal: the fields render, they are
|
|
58
|
+
* simply not gating anything.
|
|
59
|
+
*/
|
|
60
|
+
const FormReadinessContext = createContext(void 0);
|
|
61
|
+
const FormReadinessProvider = FormReadinessContext.Provider;
|
|
62
|
+
/**
|
|
63
|
+
* Track which children are still waiting for their fields.
|
|
64
|
+
*
|
|
65
|
+
* A SET keyed by the child's own `useId`, not a counter: a child that re-renders
|
|
66
|
+
* while pending must not increment twice, and one that unmounts mid-flight must
|
|
67
|
+
* not leave the form disabled forever. Both are the ordinary lifecycle here — a
|
|
68
|
+
* page that switches workflows swaps one `<WorkflowFields>` for another.
|
|
69
|
+
*/
|
|
70
|
+
function useFormReadiness() {
|
|
71
|
+
const [waiting, setWaiting] = useState(() => /* @__PURE__ */ new Set());
|
|
72
|
+
const declare = useCallback((key, pending) => {
|
|
73
|
+
setWaiting((held) => {
|
|
74
|
+
if (pending === held.has(key)) return held;
|
|
75
|
+
const next = new Set(held);
|
|
76
|
+
if (pending) next.add(key);
|
|
77
|
+
else next.delete(key);
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
80
|
+
}, []);
|
|
81
|
+
return {
|
|
82
|
+
pending: waiting.size > 0,
|
|
83
|
+
declare
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Declare from a child that its fields are, or are no longer, still loading.
|
|
88
|
+
*
|
|
89
|
+
* Reports through an EFFECT rather than during render, because this writes to a
|
|
90
|
+
* parent's state — doing it in the render body is the "cannot update a component
|
|
91
|
+
* while rendering a different component" warning, and under StrictMode it is a
|
|
92
|
+
* double report the parent has to be idempotent about anyway. The cleanup
|
|
93
|
+
* releases the claim, so an unmounted field set never holds the form shut.
|
|
94
|
+
*/
|
|
95
|
+
function useDeclareFieldsPending(pending) {
|
|
96
|
+
const declare = useContext(FormReadinessContext);
|
|
97
|
+
const key = useId();
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!declare) return;
|
|
100
|
+
declare(key, pending);
|
|
101
|
+
return () => declare(key, false);
|
|
102
|
+
}, [
|
|
103
|
+
declare,
|
|
104
|
+
key,
|
|
105
|
+
pending
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
23
109
|
//#region components/_form-values.ts
|
|
24
110
|
/**
|
|
25
111
|
* Read one `<form>`'s named controls into a plain object.
|
|
@@ -172,10 +258,11 @@ function dataUrl(file) {
|
|
|
172
258
|
function Form({ onSubmit, error, children, className, ...rest }) {
|
|
173
259
|
const theme = useTheme();
|
|
174
260
|
const [busy, setBusy] = useState(false);
|
|
261
|
+
const { pending: fieldsPending, declare } = useFormReadiness();
|
|
175
262
|
return /* @__PURE__ */ jsxs("form", {
|
|
176
263
|
onSubmit: useCallback((event) => {
|
|
177
264
|
event.preventDefault();
|
|
178
|
-
if (busy) return;
|
|
265
|
+
if (busy || fieldsPending) return;
|
|
179
266
|
const form = event.currentTarget;
|
|
180
267
|
setBusy(true);
|
|
181
268
|
(async () => {
|
|
@@ -185,13 +272,20 @@ function Form({ onSubmit, error, children, className, ...rest }) {
|
|
|
185
272
|
setBusy(false);
|
|
186
273
|
}
|
|
187
274
|
})();
|
|
188
|
-
}, [
|
|
275
|
+
}, [
|
|
276
|
+
busy,
|
|
277
|
+
fieldsPending,
|
|
278
|
+
onSubmit
|
|
279
|
+
]),
|
|
189
280
|
className: clsx("flex flex-col gap-5 font-aai", className),
|
|
190
281
|
...rest,
|
|
191
282
|
children: [/* @__PURE__ */ jsx("fieldset", {
|
|
192
|
-
disabled: busy,
|
|
283
|
+
disabled: busy || fieldsPending,
|
|
193
284
|
className: "contents",
|
|
194
|
-
children
|
|
285
|
+
children: /* @__PURE__ */ jsx(FormReadinessProvider, {
|
|
286
|
+
value: declare,
|
|
287
|
+
children
|
|
288
|
+
})
|
|
195
289
|
}), error !== void 0 && error !== "" && /* @__PURE__ */ jsx("p", {
|
|
196
290
|
role: "alert",
|
|
197
291
|
className: "text-sm",
|
|
@@ -1234,8 +1328,9 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1234
1328
|
* @public
|
|
1235
1329
|
*/
|
|
1236
1330
|
function WorkflowFields({ workflow }) {
|
|
1237
|
-
const { workflows } = useWorkflows(typeof workflow === "string" ? {} : { skip: true });
|
|
1331
|
+
const { workflows, loading } = useWorkflows(typeof workflow === "string" ? {} : { skip: true });
|
|
1238
1332
|
const summary = typeof workflow === "string" ? workflows.find((entry) => entry.name === workflow) : workflow;
|
|
1333
|
+
useDeclareFieldsPending(loading);
|
|
1239
1334
|
const schema = asObjectSchema(summary?.inputSchema);
|
|
1240
1335
|
if (!schema?.properties) return null;
|
|
1241
1336
|
const required = new Set(schema.required ?? []);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-ui",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"remark-gfm": "^4.0.1",
|
|
30
30
|
"use-stick-to-bottom": "^1.1.6",
|
|
31
31
|
"use-sync-external-store": "^1.6.0",
|
|
32
|
-
"@alexkroman1/aai": "6.7.
|
|
32
|
+
"@alexkroman1/aai": "6.7.2"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^19.0.0",
|