@datalayer/agent-runtimes 1.3.82 → 1.3.84
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/lib/chat/base/ChatBase.js +16 -0
- package/lib/chat/base/loopElement.d.ts +53 -0
- package/lib/chat/base/loopElement.js +78 -0
- package/lib/chat/base/loopWindow.d.ts +31 -0
- package/lib/chat/base/loopWindow.js +74 -0
- package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
- package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
- package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
- package/lib/components/a2ui/datalayer/Form.js +26 -2
- package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
- package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
- package/lib/loop/apps/AppComposer.d.ts +39 -4
- package/lib/loop/apps/AppComposer.js +191 -19
- package/lib/loop/apps/AppRenderer.d.ts +20 -3
- package/lib/loop/apps/AppRenderer.js +58 -27
- package/lib/loop/apps/appspec.d.ts +2 -0
- package/lib/loop/apps/appspec.js +196 -39
- package/lib/loop/apps/checks.d.ts +6 -0
- package/lib/loop/apps/checks.js +187 -8
- package/lib/loop/apps/composer.d.ts +21 -10
- package/lib/loop/apps/composer.js +41 -8
- package/lib/loop/apps/language.d.ts +68 -0
- package/lib/loop/apps/language.js +275 -0
- package/lib/loop/apps/settingsInputs.d.ts +29 -0
- package/lib/loop/apps/settingsInputs.js +147 -0
- package/lib/loop/apps/uploads.d.ts +43 -0
- package/lib/loop/apps/uploads.js +66 -0
- package/lib/loop/core/index.d.ts +14 -1
- package/lib/loop/embed/AppEmbed.d.ts +14 -0
- package/lib/loop/embed/AppEmbed.js +64 -2
- package/lib/loop/embed/element.js +23 -0
- package/lib/loop/embed/embedSession.d.ts +14 -0
- package/lib/loop/embed/embedSession.js +34 -0
- package/lib/loop/embed/hostBridge.d.ts +22 -1
- package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
- package/lib/loop/plugins/app-elements/AppElements.js +76 -0
- package/lib/loop/plugins/app-elements/index.d.ts +20 -0
- package/lib/loop/plugins/app-elements/index.js +43 -0
- package/lib/loop/plugins/app-page/appPageModel.d.ts +14 -5
- package/lib/loop/plugins/app-page/appPageModel.js +82 -88
- package/lib/loop/plugins/chat/ChatView.js +10 -2
- package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
- package/lib/protocols/BrowserAgentAdapter.js +13 -10
- package/lib/runtimes/browser/protocol.d.ts +1 -1
- package/lib/specs/agents/agents.js +6 -3
- package/lib/specs/apps.js +337 -176
- package/lib/specs/appspecSchema.js +455 -88
- package/lib/specs/cogs.js +3 -2
- package/lib/specs/uiPlugins.js +6 -1
- package/lib/types/agentspecs.d.ts +167 -11
- package/package.json +1 -1
- package/scripts/codegen/generate_apps.py +16 -1
|
@@ -62,6 +62,8 @@ import { useAgentInspectorSink } from '../../components/inspector/agentInspector
|
|
|
62
62
|
import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
|
|
63
63
|
import { loopMessageChange, speakerOf, withLoopMessage } from './loopMessage';
|
|
64
64
|
import { loopStepOf, withLoopStep } from './loopStep';
|
|
65
|
+
import { applyLoopElement, loopElementChange } from './loopElement';
|
|
66
|
+
import { loopWindowMessage, tellLoopWindow } from './loopWindow';
|
|
65
67
|
// Tracks pending prompts already auto-sent for a given conversation scope.
|
|
66
68
|
// This prevents layout-driven unmount/remount cycles from re-sending prompts.
|
|
67
69
|
const sentPendingPromptKeys = new Set();
|
|
@@ -2724,6 +2726,20 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2724
2726
|
setDisplayItems(prev => withLoopStep(prev, step));
|
|
2725
2727
|
break;
|
|
2726
2728
|
}
|
|
2729
|
+
// What its code tells the page it sits in, outside the
|
|
2730
|
+
// conversation (LOOP P-25): handed to whoever listens.
|
|
2731
|
+
const windowMessage = loopWindowMessage(event.activity);
|
|
2732
|
+
if (windowMessage) {
|
|
2733
|
+
tellLoopWindow(windowMessage.data);
|
|
2734
|
+
break;
|
|
2735
|
+
}
|
|
2736
|
+
// An element its code opened in a side panel or on a page of its
|
|
2737
|
+
// own, changed or closed (LOOP P-18): kept for whoever draws them.
|
|
2738
|
+
const element = loopElementChange(event.activity);
|
|
2739
|
+
if (element) {
|
|
2740
|
+
applyLoopElement(element);
|
|
2741
|
+
break;
|
|
2742
|
+
}
|
|
2727
2743
|
// An application's code changed a message it sent: its author
|
|
2728
2744
|
// said, or the message taken away (LOOP P-15).
|
|
2729
2745
|
const change = loopMessageChange(event.activity);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an element of an application's code shows (LOOP P-18): in a side
|
|
3
|
+
* panel beside the conversation, or on a page of its own over it. Inline,
|
|
4
|
+
* an element is a message, and the chat draws it as one (P-04).
|
|
5
|
+
*
|
|
6
|
+
* The session API says it with a `CUSTOM` event named `loop.element` —
|
|
7
|
+
* `{id, where, title, shows}`, `shows` the A2UI surface an answer's
|
|
8
|
+
* components are drawn from, or `{id, closed: true}` — which the AG-UI
|
|
9
|
+
* adapter hands on as an `activity`. The chat does not draw it: it keeps
|
|
10
|
+
* what is open here, and whoever draws panels and pages (the `app-elements`
|
|
11
|
+
* plugin) listens. One store per page, as there is one chat per
|
|
12
|
+
* application on it.
|
|
13
|
+
*/
|
|
14
|
+
/** The `CUSTOM` event's name. */
|
|
15
|
+
export declare const LOOP_ELEMENT = "loop.element";
|
|
16
|
+
/** Where an element opens, besides inline. */
|
|
17
|
+
export type LoopElementWhere = 'panel' | 'page';
|
|
18
|
+
/** An element open in a side panel or on a page. */
|
|
19
|
+
export type LoopElement = {
|
|
20
|
+
id: string;
|
|
21
|
+
where: LoopElementWhere;
|
|
22
|
+
title: string;
|
|
23
|
+
/** The surface it draws: what `render_a2ui_surface` returns. */
|
|
24
|
+
shows: unknown;
|
|
25
|
+
};
|
|
26
|
+
/** What an event says: an element opened or changed, or one closed. */
|
|
27
|
+
export type LoopElementChange = LoopElement | {
|
|
28
|
+
id: string;
|
|
29
|
+
closed: true;
|
|
30
|
+
};
|
|
31
|
+
/** The change an activity says, or `null` when it says none. */
|
|
32
|
+
export declare function loopElementChange(activity: {
|
|
33
|
+
type: string;
|
|
34
|
+
data: unknown;
|
|
35
|
+
} | undefined): LoopElementChange | null;
|
|
36
|
+
/**
|
|
37
|
+
* The elements open with a change applied: one opened is added last, one
|
|
38
|
+
* changed is replaced where it is, one closed is taken away.
|
|
39
|
+
*/
|
|
40
|
+
export declare function withLoopElement(open: readonly LoopElement[], change: LoopElementChange): LoopElement[];
|
|
41
|
+
/** The elements open now, in the order they opened. */
|
|
42
|
+
export declare const openLoopElements: () => LoopElement[];
|
|
43
|
+
/** Apply a change, and tell whoever draws them. */
|
|
44
|
+
export declare function applyLoopElement(change: LoopElementChange): void;
|
|
45
|
+
/**
|
|
46
|
+
* Close an element on this screen: the person closed it. The code is not
|
|
47
|
+
* told; it may show it again.
|
|
48
|
+
*/
|
|
49
|
+
export declare const closeLoopElement: (id: string) => void;
|
|
50
|
+
/** Close every element: a new conversation starts with none. */
|
|
51
|
+
export declare function clearLoopElements(): void;
|
|
52
|
+
/** Listen for what is open. Returns the unsubscribe. */
|
|
53
|
+
export declare function onLoopElements(listener: (open: LoopElement[]) => void): () => void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Where an element of an application's code shows (LOOP P-18): in a side
|
|
7
|
+
* panel beside the conversation, or on a page of its own over it. Inline,
|
|
8
|
+
* an element is a message, and the chat draws it as one (P-04).
|
|
9
|
+
*
|
|
10
|
+
* The session API says it with a `CUSTOM` event named `loop.element` —
|
|
11
|
+
* `{id, where, title, shows}`, `shows` the A2UI surface an answer's
|
|
12
|
+
* components are drawn from, or `{id, closed: true}` — which the AG-UI
|
|
13
|
+
* adapter hands on as an `activity`. The chat does not draw it: it keeps
|
|
14
|
+
* what is open here, and whoever draws panels and pages (the `app-elements`
|
|
15
|
+
* plugin) listens. One store per page, as there is one chat per
|
|
16
|
+
* application on it.
|
|
17
|
+
*/
|
|
18
|
+
/** The `CUSTOM` event's name. */
|
|
19
|
+
export const LOOP_ELEMENT = 'loop.element';
|
|
20
|
+
/** The change an activity says, or `null` when it says none. */
|
|
21
|
+
export function loopElementChange(activity) {
|
|
22
|
+
if (!activity || activity.type !== LOOP_ELEMENT)
|
|
23
|
+
return null;
|
|
24
|
+
const data = activity.data;
|
|
25
|
+
if (!data || typeof data.id !== 'string' || !data.id)
|
|
26
|
+
return null;
|
|
27
|
+
if (data.closed === true)
|
|
28
|
+
return { id: data.id, closed: true };
|
|
29
|
+
if (data.where !== 'panel' && data.where !== 'page')
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
id: data.id,
|
|
33
|
+
where: data.where,
|
|
34
|
+
title: typeof data.title === 'string' ? data.title : '',
|
|
35
|
+
shows: data.shows ?? null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The elements open with a change applied: one opened is added last, one
|
|
40
|
+
* changed is replaced where it is, one closed is taken away.
|
|
41
|
+
*/
|
|
42
|
+
export function withLoopElement(open, change) {
|
|
43
|
+
if ('closed' in change) {
|
|
44
|
+
return open.filter(element => element.id !== change.id);
|
|
45
|
+
}
|
|
46
|
+
const at = open.findIndex(element => element.id === change.id);
|
|
47
|
+
if (at < 0)
|
|
48
|
+
return [...open, change];
|
|
49
|
+
return open.map((element, index) => (index === at ? change : element));
|
|
50
|
+
}
|
|
51
|
+
let opened = [];
|
|
52
|
+
const listeners = new Set();
|
|
53
|
+
/** The elements open now, in the order they opened. */
|
|
54
|
+
export const openLoopElements = () => opened;
|
|
55
|
+
/** Apply a change, and tell whoever draws them. */
|
|
56
|
+
export function applyLoopElement(change) {
|
|
57
|
+
opened = withLoopElement(opened, change);
|
|
58
|
+
for (const listener of [...listeners])
|
|
59
|
+
listener(opened);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Close an element on this screen: the person closed it. The code is not
|
|
63
|
+
* told; it may show it again.
|
|
64
|
+
*/
|
|
65
|
+
export const closeLoopElement = (id) => applyLoopElement({ id, closed: true });
|
|
66
|
+
/** Close every element: a new conversation starts with none. */
|
|
67
|
+
export function clearLoopElements() {
|
|
68
|
+
opened = [];
|
|
69
|
+
for (const listener of [...listeners])
|
|
70
|
+
listener(opened);
|
|
71
|
+
}
|
|
72
|
+
/** Listen for what is open. Returns the unsubscribe. */
|
|
73
|
+
export function onLoopElements(listener) {
|
|
74
|
+
listeners.add(listener);
|
|
75
|
+
return () => {
|
|
76
|
+
listeners.delete(listener);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window messages (LOOP P-25): what an application's code tells the page it
|
|
3
|
+
* sits in, outside the conversation — ``session.send_window_message(data)``.
|
|
4
|
+
*
|
|
5
|
+
* The session API says it with a `CUSTOM` event named `loop.window` —
|
|
6
|
+
* `{data}` — which the AG-UI adapter hands on as an `activity`. The chat
|
|
7
|
+
* does not draw it: it hands it to whoever listens here — the embed's host
|
|
8
|
+
* bridge, which raises it on the element as its `window-message` event.
|
|
9
|
+
*/
|
|
10
|
+
/** The `CUSTOM` event's name. */
|
|
11
|
+
export declare const LOOP_WINDOW = "loop.window";
|
|
12
|
+
/** What an activity says to the page, or `null` when it is not a window message. */
|
|
13
|
+
export declare function loopWindowMessage(activity: {
|
|
14
|
+
type: string;
|
|
15
|
+
data: unknown;
|
|
16
|
+
} | undefined): {
|
|
17
|
+
data: unknown;
|
|
18
|
+
} | null;
|
|
19
|
+
/** Hand a window message to whoever listens. */
|
|
20
|
+
export declare function tellLoopWindow(data: unknown): void;
|
|
21
|
+
/** Listen for window messages. Returns the unsubscribe. */
|
|
22
|
+
export declare function onLoopWindowMessage(listener: (data: unknown) => void): () => void;
|
|
23
|
+
/**
|
|
24
|
+
* What a turn's stream says to the page — the `loop.window` events of the
|
|
25
|
+
* turn a window message posted by the page ran — and the sentence its
|
|
26
|
+
* `RUN_ERROR` said, if it failed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function windowTurnOf(stream: string): {
|
|
29
|
+
said: unknown[];
|
|
30
|
+
error: string | null;
|
|
31
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Window messages (LOOP P-25): what an application's code tells the page it
|
|
7
|
+
* sits in, outside the conversation — ``session.send_window_message(data)``.
|
|
8
|
+
*
|
|
9
|
+
* The session API says it with a `CUSTOM` event named `loop.window` —
|
|
10
|
+
* `{data}` — which the AG-UI adapter hands on as an `activity`. The chat
|
|
11
|
+
* does not draw it: it hands it to whoever listens here — the embed's host
|
|
12
|
+
* bridge, which raises it on the element as its `window-message` event.
|
|
13
|
+
*/
|
|
14
|
+
/** The `CUSTOM` event's name. */
|
|
15
|
+
export const LOOP_WINDOW = 'loop.window';
|
|
16
|
+
/** What an activity says to the page, or `null` when it is not a window message. */
|
|
17
|
+
export function loopWindowMessage(activity) {
|
|
18
|
+
if (!activity || activity.type !== LOOP_WINDOW)
|
|
19
|
+
return null;
|
|
20
|
+
const said = activity.data;
|
|
21
|
+
if (!said || typeof said !== 'object' || !('data' in said))
|
|
22
|
+
return null;
|
|
23
|
+
return { data: said.data };
|
|
24
|
+
}
|
|
25
|
+
const listeners = new Set();
|
|
26
|
+
/** Hand a window message to whoever listens. */
|
|
27
|
+
export function tellLoopWindow(data) {
|
|
28
|
+
for (const listener of [...listeners])
|
|
29
|
+
listener(data);
|
|
30
|
+
}
|
|
31
|
+
/** Listen for window messages. Returns the unsubscribe. */
|
|
32
|
+
export function onLoopWindowMessage(listener) {
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
return () => {
|
|
35
|
+
listeners.delete(listener);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What a turn's stream says to the page — the `loop.window` events of the
|
|
40
|
+
* turn a window message posted by the page ran — and the sentence its
|
|
41
|
+
* `RUN_ERROR` said, if it failed.
|
|
42
|
+
*/
|
|
43
|
+
export function windowTurnOf(stream) {
|
|
44
|
+
const said = [];
|
|
45
|
+
let error = null;
|
|
46
|
+
for (const block of stream.split('\n\n')) {
|
|
47
|
+
const data = block
|
|
48
|
+
.split('\n')
|
|
49
|
+
.filter(line => line.startsWith('data:'))
|
|
50
|
+
.map(line => line.slice('data:'.length).trim())
|
|
51
|
+
.join('');
|
|
52
|
+
if (!data)
|
|
53
|
+
continue;
|
|
54
|
+
let event;
|
|
55
|
+
try {
|
|
56
|
+
event = JSON.parse(data);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (event.type === 'RUN_ERROR') {
|
|
62
|
+
error = String(event.message ?? 'The turn failed.');
|
|
63
|
+
}
|
|
64
|
+
else if (event.type === 'CUSTOM') {
|
|
65
|
+
const message = loopWindowMessage({
|
|
66
|
+
type: String(event.name),
|
|
67
|
+
data: event.value,
|
|
68
|
+
});
|
|
69
|
+
if (message)
|
|
70
|
+
said.push(message.data);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { said, error };
|
|
74
|
+
}
|
|
@@ -14,8 +14,16 @@ export type GivenFile = {
|
|
|
14
14
|
size: number;
|
|
15
15
|
data_url: string;
|
|
16
16
|
};
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Why a file is refused, or `null` when it is taken: its kinds are
|
|
19
|
+
* extensions (`.csv`), media types (`application/pdf`) or families
|
|
20
|
+
* (`image/*`), as the Appspec's uploads say them (LOOP P-21).
|
|
21
|
+
*/
|
|
22
|
+
export declare function refusal(file: Pick<File, 'name' | 'size'> & {
|
|
23
|
+
type?: string;
|
|
24
|
+
}, accept: string[] | undefined, maxMb: number): string | null;
|
|
25
|
+
/** A file as the application receives it: its content as a data URL. */
|
|
26
|
+
export declare const readFile: (file: File) => Promise<GivenFile>;
|
|
19
27
|
export declare function FileUploadView({ props }: {
|
|
20
28
|
props: FileUploadProps;
|
|
21
29
|
}): import("react").JSX.Element;
|
|
@@ -21,22 +21,25 @@ import { UploadIcon } from '@primer/octicons-react';
|
|
|
21
21
|
import { Box } from '@datalayer/primer-addons';
|
|
22
22
|
import { BlockFrame, CARD_RADIUS, Problem, Quiet, isRecord } from './parts';
|
|
23
23
|
import { ownImplementation } from './implementation';
|
|
24
|
+
import { acceptsFile } from '../../../loop/apps/uploads';
|
|
24
25
|
/** The default the catalog gives `max_mb`. */
|
|
25
26
|
const MAX_MB = 25;
|
|
26
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Why a file is refused, or `null` when it is taken: its kinds are
|
|
29
|
+
* extensions (`.csv`), media types (`application/pdf`) or families
|
|
30
|
+
* (`image/*`), as the Appspec's uploads say them (LOOP P-21).
|
|
31
|
+
*/
|
|
27
32
|
export function refusal(file, accept, maxMb) {
|
|
28
|
-
if (accept
|
|
29
|
-
|
|
30
|
-
if (!accept.some(extension => lower.endsWith(extension.toLowerCase()))) {
|
|
31
|
-
return `${file.name} is not one it takes (${accept.join(', ')}).`;
|
|
32
|
-
}
|
|
33
|
+
if (!acceptsFile(accept, { name: file.name, type: file.type ?? '' })) {
|
|
34
|
+
return `${file.name} is not one it takes (${(accept ?? []).join(', ')}).`;
|
|
33
35
|
}
|
|
34
36
|
if (file.size > maxMb * 1024 * 1024) {
|
|
35
37
|
return `${file.name} is larger than ${maxMb} MB.`;
|
|
36
38
|
}
|
|
37
39
|
return null;
|
|
38
40
|
}
|
|
39
|
-
|
|
41
|
+
/** A file as the application receives it: its content as a data URL. */
|
|
42
|
+
export const readFile = (file) => new Promise((resolve, reject) => {
|
|
40
43
|
const reader = new FileReader();
|
|
41
44
|
reader.onload = () => resolve({
|
|
42
45
|
name: file.name,
|
|
@@ -2,6 +2,8 @@ import { type OwnCommon } from './implementation';
|
|
|
2
2
|
export type FormProps = OwnCommon & {
|
|
3
3
|
title?: string;
|
|
4
4
|
schema: Record<string, unknown>;
|
|
5
|
+
/** How its fields are drawn: a uiSchema by field name (P-20). */
|
|
6
|
+
ui?: Record<string, unknown>;
|
|
5
7
|
submit_label?: string;
|
|
6
8
|
values?: unknown;
|
|
7
9
|
setValues: (values: Record<string, unknown>) => void;
|
|
@@ -12,6 +12,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
12
12
|
* its action is dispatched, so the action's context reads them. A form the
|
|
13
13
|
* schema refuses says why beside each field and sends nothing.
|
|
14
14
|
*
|
|
15
|
+
* A form without an action is a set of settings (C-16) — an application's
|
|
16
|
+
* `interface.settings` drawn beside its conversation: no button, its values
|
|
17
|
+
* written where `values` points as they are filled, for the next run to take;
|
|
18
|
+
* the runtime checks them against the same schema.
|
|
19
|
+
*
|
|
20
|
+
* Its `ui` says how its fields are drawn (LOOP P-20): a uiSchema by field
|
|
21
|
+
* name, `ui:widget` one of primer-rjsf's widgets or of `switch` and `tags`
|
|
22
|
+
* (`formWidgets`) — the nine inputs a setting is drawn with.
|
|
23
|
+
*
|
|
15
24
|
* @module components/a2ui/datalayer/Form
|
|
16
25
|
*/
|
|
17
26
|
import { useEffect, useMemo, useState } from 'react';
|
|
@@ -19,6 +28,7 @@ import { Form as SchemaForm } from '@datalayer/primer-rjsf';
|
|
|
19
28
|
import validator from '@rjsf/validator-ajv8';
|
|
20
29
|
import { BlockFrame, Problem, isRecord } from './parts';
|
|
21
30
|
import { ownImplementation } from './implementation';
|
|
31
|
+
import { FORM_WIDGETS } from './formWidgets';
|
|
22
32
|
/** The default the catalog gives `submit_label`. */
|
|
23
33
|
const SUBMIT_LABEL = 'Send';
|
|
24
34
|
export function FormView({ props }) {
|
|
@@ -34,11 +44,25 @@ export function FormView({ props }) {
|
|
|
34
44
|
useEffect(() => {
|
|
35
45
|
setLocal(JSON.parse(shownKey));
|
|
36
46
|
}, [shownKey]);
|
|
37
|
-
|
|
47
|
+
// Without an action, nothing is sent: no button, the values written as filled.
|
|
48
|
+
const live = action === undefined;
|
|
49
|
+
const drawn = JSON.stringify(props.ui ?? {});
|
|
50
|
+
const uiSchema = useMemo(() => ({
|
|
51
|
+
...JSON.parse(drawn),
|
|
52
|
+
'ui:submitButtonOptions': live
|
|
53
|
+
? { norender: true }
|
|
54
|
+
: { submitText: submitLabel },
|
|
55
|
+
}), [drawn, live, submitLabel]);
|
|
38
56
|
const problem = schema.type !== undefined && schema.type !== 'object'
|
|
39
57
|
? 'Its fields are not an object schema: a form asks for several named values.'
|
|
40
58
|
: null;
|
|
41
|
-
return (_jsx(BlockFrame, { title: title, label: "Form", weight: props.weight, testId: "a2ui-form", children: problem ? (_jsx(Problem, { children: problem })) : (_jsx(SchemaForm, { schema: schema, uiSchema: uiSchema, formData: values, validator: validator, liveValidate: touched, onBlur: () => setTouched(true), onError: () => setTouched(true), showErrorList: false, onChange: event =>
|
|
59
|
+
return (_jsx(BlockFrame, { title: title, label: "Form", weight: props.weight, testId: "a2ui-form", children: problem ? (_jsx(Problem, { children: problem })) : (_jsx(SchemaForm, { schema: schema, uiSchema: uiSchema, widgets: FORM_WIDGETS, formData: values, validator: validator, liveValidate: touched, onBlur: () => setTouched(true), onError: () => setTouched(true), showErrorList: false, onChange: event => {
|
|
60
|
+
const filled = (event.formData ?? {});
|
|
61
|
+
setLocal(filled);
|
|
62
|
+
if (live) {
|
|
63
|
+
setValues(filled);
|
|
64
|
+
}
|
|
65
|
+
}, onSubmit: event => {
|
|
42
66
|
setValues((event.formData ?? {}));
|
|
43
67
|
action?.();
|
|
44
68
|
} })) }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** What rjsf gives a widget, of what these two read. */
|
|
2
|
+
export type WidgetProps = {
|
|
3
|
+
id: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
readonly?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
onChange: (value: unknown) => void;
|
|
10
|
+
};
|
|
11
|
+
/** A `boolean` as a toggle switch. */
|
|
12
|
+
export declare function SwitchWidget({ id, value, label, disabled, readonly, onChange, }: WidgetProps): import("react").JSX.Element;
|
|
13
|
+
/** An `array` of `string` items as tokens: typed, then Enter or a comma. */
|
|
14
|
+
export declare function TagsWidget({ id, value, label, disabled, readonly, placeholder, onChange, }: WidgetProps): import("react").JSX.Element;
|
|
15
|
+
/** What a form's uiSchema may name besides primer-rjsf's own widgets. */
|
|
16
|
+
export declare const FORM_WIDGETS: {
|
|
17
|
+
switch: typeof SwitchWidget;
|
|
18
|
+
tags: typeof TagsWidget;
|
|
19
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The two inputs of the nine (LOOP P-20) that `@datalayer/primer-rjsf` does
|
|
8
|
+
* not draw itself, as rjsf widgets a form's uiSchema names (`ui:widget`):
|
|
9
|
+
*
|
|
10
|
+
* - `switch` — a `boolean` as Primer's toggle switch;
|
|
11
|
+
* - `tags` — an `array` of `string` items as words in a field, each a token:
|
|
12
|
+
* Enter or a comma adds what is typed, a token's cross takes it away.
|
|
13
|
+
*
|
|
14
|
+
* The seven others are primer-rjsf's: Select (an `enum`), Slider (`range`),
|
|
15
|
+
* TextInput (a `string`, or `textarea`), Checkbox (a `boolean`), DatePicker
|
|
16
|
+
* (`format: date`), MultiSelect (an `array` of `enum` items, or
|
|
17
|
+
* `checkboxes`) and RadioGroup (`radio`).
|
|
18
|
+
*
|
|
19
|
+
* @module components/a2ui/datalayer/formWidgets
|
|
20
|
+
*/
|
|
21
|
+
import { useState } from 'react';
|
|
22
|
+
import { Box, FormControl, Text, TextInputWithTokens, ToggleSwitch, } from '@primer/react';
|
|
23
|
+
/** A `boolean` as a toggle switch. */
|
|
24
|
+
export function SwitchWidget({ id, value, label, disabled, readonly, onChange, }) {
|
|
25
|
+
return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsx(Text, { as: "span", id: `${id}-label`, sx: { fontWeight: 'semibold' }, children: label }), _jsx(ToggleSwitch, { size: "small", "aria-labelledby": `${id}-label`, checked: Boolean(value), disabled: disabled || readonly, onClick: () => onChange(!value) })] }));
|
|
26
|
+
}
|
|
27
|
+
/** An `array` of `string` items as tokens: typed, then Enter or a comma. */
|
|
28
|
+
export function TagsWidget({ id, value, label, disabled, readonly, placeholder, onChange, }) {
|
|
29
|
+
const tags = Array.isArray(value) ? value.map(String) : [];
|
|
30
|
+
const [typed, setTyped] = useState('');
|
|
31
|
+
const add = () => {
|
|
32
|
+
const word = typed.trim();
|
|
33
|
+
if (word && !tags.includes(word)) {
|
|
34
|
+
onChange([...tags, word]);
|
|
35
|
+
}
|
|
36
|
+
setTyped('');
|
|
37
|
+
};
|
|
38
|
+
return (_jsxs(FormControl, { id: id, disabled: disabled || readonly, children: [_jsx(FormControl.Label, { children: label }), _jsx(TextInputWithTokens, { block: true, placeholder: placeholder, tokens: tags.map((text, index) => ({ id: index, text })), value: typed, onChange: event => setTyped(event.target.value.replace(',', '')), onKeyDown: event => {
|
|
39
|
+
if (event.key === 'Enter' || event.key === ',') {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
add();
|
|
42
|
+
}
|
|
43
|
+
}, onBlur: add, onTokenRemove: index => {
|
|
44
|
+
onChange(tags.filter((_, at) => at !== index));
|
|
45
|
+
} })] }));
|
|
46
|
+
}
|
|
47
|
+
/** What a form's uiSchema may name besides primer-rjsf's own widgets. */
|
|
48
|
+
export const FORM_WIDGETS = {
|
|
49
|
+
switch: SwitchWidget,
|
|
50
|
+
tags: TagsWidget,
|
|
51
|
+
};
|
|
@@ -9,17 +9,52 @@
|
|
|
9
9
|
* one menu each; the option picked goes with every run as
|
|
10
10
|
* `forwardedProps.loop.modes` (`LoopRunProps`), and the runtime tells the
|
|
11
11
|
* agent its instructions, on the model it names, for that run.
|
|
12
|
+
* - What a person may **send** without being asked (LOOP P-21,
|
|
13
|
+
* `interface.uploads`): a paper clip beside the composer takes images,
|
|
14
|
+
* files and recordings of the kinds and sizes the Appspec says — one it
|
|
15
|
+
* does not take is refused in a sentence and never read — and the files
|
|
16
|
+
* held go with the next message as `forwardedProps.loop.files`, then are
|
|
17
|
+
* let go. The runtime refuses again what was sent all the same.
|
|
18
|
+
* - Its **profiles** (LOOP P-20, `interface.profiles`): a picker beside the
|
|
19
|
+
* composer, on the first unless another is picked. The profile picked goes
|
|
20
|
+
* with every run as `forwardedProps.loop.profile`, and its starters (else
|
|
21
|
+
* the application's) are what the empty chat offers, by category
|
|
22
|
+
* (`LoopChatExtras.openers`). Once the first message is sent, the
|
|
23
|
+
* conversation keeps its profile: the picker says so and picks no other.
|
|
24
|
+
*
|
|
25
|
+
* Given the application as the person reads it (`translatedAppspec`, LOOP
|
|
26
|
+
* P-26), its words are theirs; the composer's own, `language`'s.
|
|
12
27
|
*
|
|
13
28
|
* @module loop/apps/AppComposer
|
|
14
29
|
*/
|
|
15
|
-
import type
|
|
16
|
-
import type { AppSpec } from '../../types/agentspecs';
|
|
30
|
+
import { type JSX } from 'react';
|
|
31
|
+
import type { AppSpec, AppStarterSpec } from '../../types/agentspecs';
|
|
32
|
+
import { type GivenFile } from '../../components/a2ui/datalayer/FileUpload';
|
|
33
|
+
import { type ChatSuggestionItem } from '../core';
|
|
34
|
+
/** What the composer holds to send: the files, and why the last were refused. */
|
|
35
|
+
export type HeldUploads = {
|
|
36
|
+
files: GivenFile[];
|
|
37
|
+
refused: string | null;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Files chosen in the composer, held for the next message: the refusal the
|
|
41
|
+
* runtime would say for what the application does not take, else the files
|
|
42
|
+
* read. Nothing is read when one is refused.
|
|
43
|
+
*/
|
|
44
|
+
export declare function holdUploads(app: Pick<AppSpec, 'name' | 'interface'>, held: readonly GivenFile[], chosen: readonly File[], read?: (file: File) => Promise<GivenFile>): Promise<HeldUploads>;
|
|
17
45
|
/** The plugin's name, before the application's id. */
|
|
18
46
|
export declare const APP_COMPOSER_PLUGIN_NAME = "@datalayer/loop-plugin-app-composer";
|
|
47
|
+
/** Starters as the empty chat offers them: each under its category (P-20). */
|
|
48
|
+
export declare function startersAsOpeners(starters: readonly AppStarterSpec[]): ChatSuggestionItem[];
|
|
49
|
+
/**
|
|
50
|
+
* Whether an application needs its composer plugin: commands, modes,
|
|
51
|
+
* uploads or profiles to offer there.
|
|
52
|
+
*/
|
|
53
|
+
export declare const needsAppComposer: (app: AppSpec) => boolean;
|
|
19
54
|
/**
|
|
20
55
|
* The plugin that puts an application's commands in the composer's `/` menu
|
|
21
|
-
* and its modes
|
|
22
|
-
*
|
|
56
|
+
* and its modes, its profiles and its paper clip beside the composer.
|
|
57
|
+
* Nothing for an application with none: its plugin contributes nothing.
|
|
23
58
|
*/
|
|
24
59
|
export declare function defineAppComposerPlugin(app: AppSpec): import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, {
|
|
25
60
|
components: {
|