@datalayer/agent-runtimes 1.3.83 → 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.
Files changed (49) hide show
  1. package/lib/chat/base/ChatBase.js +16 -0
  2. package/lib/chat/base/loopElement.d.ts +53 -0
  3. package/lib/chat/base/loopElement.js +78 -0
  4. package/lib/chat/base/loopWindow.d.ts +31 -0
  5. package/lib/chat/base/loopWindow.js +74 -0
  6. package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
  7. package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
  8. package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
  9. package/lib/components/a2ui/datalayer/Form.js +9 -2
  10. package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
  11. package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
  12. package/lib/loop/apps/AppComposer.d.ts +39 -4
  13. package/lib/loop/apps/AppComposer.js +191 -19
  14. package/lib/loop/apps/AppRenderer.d.ts +20 -3
  15. package/lib/loop/apps/AppRenderer.js +58 -27
  16. package/lib/loop/apps/appspec.d.ts +2 -0
  17. package/lib/loop/apps/appspec.js +190 -11
  18. package/lib/loop/apps/checks.d.ts +6 -0
  19. package/lib/loop/apps/checks.js +170 -1
  20. package/lib/loop/apps/composer.d.ts +21 -10
  21. package/lib/loop/apps/composer.js +41 -8
  22. package/lib/loop/apps/language.d.ts +68 -0
  23. package/lib/loop/apps/language.js +275 -0
  24. package/lib/loop/apps/settingsInputs.d.ts +29 -0
  25. package/lib/loop/apps/settingsInputs.js +147 -0
  26. package/lib/loop/apps/uploads.d.ts +43 -0
  27. package/lib/loop/apps/uploads.js +66 -0
  28. package/lib/loop/core/index.d.ts +14 -1
  29. package/lib/loop/embed/AppEmbed.d.ts +14 -0
  30. package/lib/loop/embed/AppEmbed.js +64 -2
  31. package/lib/loop/embed/element.js +23 -0
  32. package/lib/loop/embed/embedSession.d.ts +14 -0
  33. package/lib/loop/embed/embedSession.js +34 -0
  34. package/lib/loop/embed/hostBridge.d.ts +22 -1
  35. package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
  36. package/lib/loop/plugins/app-elements/AppElements.js +76 -0
  37. package/lib/loop/plugins/app-elements/index.d.ts +20 -0
  38. package/lib/loop/plugins/app-elements/index.js +43 -0
  39. package/lib/loop/plugins/app-page/appPageModel.js +2 -0
  40. package/lib/loop/plugins/chat/ChatView.js +10 -2
  41. package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
  42. package/lib/protocols/BrowserAgentAdapter.js +13 -10
  43. package/lib/runtimes/browser/protocol.d.ts +1 -1
  44. package/lib/specs/apps.js +149 -0
  45. package/lib/specs/appspecSchema.js +439 -2
  46. package/lib/specs/uiPlugins.js +6 -1
  47. package/lib/types/agentspecs.d.ts +130 -1
  48. package/package.json +1 -1
  49. package/scripts/codegen/generate_apps.py +16 -0
@@ -44,18 +44,51 @@ export function modeChoice(app, chosen = {}) {
44
44
  return { ...modeDefaults(app), ...chosen };
45
45
  }
46
46
  /**
47
- * What a run in the modes chosen is told, and the model it runs on —
48
- * agentspecs' `mode_effect`: the instructions of every option chosen, in the
49
- * order of the modes, and the model of the first that names one. Refused as
50
- * {@link modeChoice} refuses.
47
+ * The profile a conversation is with (LOOP P-20): the one chosen, else the
48
+ * first; undefined for an application without profiles. One it does not have
49
+ * is refused, in a sentence, as the runtime refuses it.
51
50
  */
52
- export function modeEffect(app, chosen = {}) {
51
+ export function profileChoice(app, chosen) {
52
+ const profiles = app.interface.profiles ?? [];
53
+ if (chosen) {
54
+ const profile = profiles.find(candidate => candidate.id === chosen);
55
+ if (!profile) {
56
+ throw new Error(profiles.length === 0
57
+ ? `${app.name} has no profiles: “${chosen}” cannot be chosen.`
58
+ : `${app.name} has no profile “${chosen}”.`);
59
+ }
60
+ return profile;
61
+ }
62
+ return profiles[0];
63
+ }
64
+ /** The starters offered with a profile: its own, else the application's (P-20). */
65
+ export function startersFor(app, profile) {
66
+ const chosen = profileChoice(app, profile);
67
+ return chosen && chosen.starters.length > 0
68
+ ? chosen.starters
69
+ : app.interface.starters;
70
+ }
71
+ /**
72
+ * What a run in the modes chosen, with the profile chosen, is told, and the
73
+ * model it runs on — agentspecs' `mode_effect`, and the runtime's
74
+ * `run_effect`: the profile's instructions first, then those of every option
75
+ * chosen, in the order of the modes; the model of the first option that
76
+ * names one, else the profile's. Refused as {@link modeChoice} and
77
+ * {@link profileChoice} refuse.
78
+ */
79
+ export function modeEffect(app, chosen = {}, profile) {
53
80
  const choice = modeChoice(app, chosen);
81
+ const withProfile = profileChoice(app, profile);
54
82
  const options = (app.interface.modes ?? []).flatMap(mode => mode.options.filter(option => option.id === choice[mode.id]));
55
- const instructions = options
56
- .map(option => (option.instructions ?? '').trim())
83
+ const instructions = [
84
+ withProfile?.instructions ?? '',
85
+ ...options.map(option => option.instructions ?? ''),
86
+ ]
87
+ .map(part => part.trim())
57
88
  .filter(Boolean)
58
89
  .join('\n\n');
59
- const model = options.find(option => option.model)?.model ?? undefined;
90
+ const model = options.find(option => option.model)?.model ??
91
+ withProfile?.model ??
92
+ undefined;
60
93
  return model ? { instructions, model } : { instructions };
61
94
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * An application in the person's language (LOOP P-26).
3
+ *
4
+ * An Appspec says its words in its own language (`interface.language`, `en`
5
+ * unless said) and may say them in others (`interface.translations`, by BCP
6
+ * 47 tag): its name, description and welcome, its starters by label and
7
+ * their categories, its settings' fields, its commands' descriptions, its
8
+ * modes and its profiles. The page shows the first language the person
9
+ * prefers that the application has — the browser's, unless its host says
10
+ * another — and its own words otherwise; what is not translated stays in its
11
+ * own words. Only what is shown changes: ids, values and prompts are the same
12
+ * in every language, and a starter's message is sent in the person's.
13
+ *
14
+ * agentspecs' `LANGUAGE_TAG`, `pick_language`, `AppSpec.translated` and the
15
+ * translation checks, said again for the page. The interface's own words —
16
+ * the composer's, the profile picker's — are {@link interfaceWords}.
17
+ *
18
+ * Pure: no React, no network ({@link preferredLanguages} reads the browser's).
19
+ *
20
+ * @module loop/apps/language
21
+ */
22
+ import type { AppInterfaceSpec, AppSpec } from '../../types/agentspecs';
23
+ /**
24
+ * A language as BCP 47 tags it: two or three letters, then optionally a
25
+ * script, a region and variants — `fr`, `pt-BR`, `zh-Hant-TW`.
26
+ */
27
+ export declare const LANGUAGE_TAG: RegExp;
28
+ /**
29
+ * The first language a person prefers that is available, or null. Tags are
30
+ * compared without case; a preferred tag also takes one of its language
31
+ * alone, then one of the same language (`fr-CA` takes `fr`, then `fr-FR`).
32
+ */
33
+ export declare function pickLanguage(available: readonly string[], preferred: readonly string[]): string | null;
34
+ /** The languages the browser says the person prefers, in their order; none outside one. */
35
+ export declare function preferredLanguages(): string[];
36
+ /** The language an application is read in by a person who prefers these. */
37
+ export declare function appLanguage(app: Pick<AppSpec, 'interface'>, preferred: readonly string[]): string;
38
+ /**
39
+ * The application as a person who prefers these languages reads it: its
40
+ * name, description and interface in the first of them it is translated
41
+ * into, else itself. What it does — its agent, its rules, its ids, its
42
+ * values, its prompts — is the same.
43
+ */
44
+ export declare function translatedAppspec(app: AppSpec, preferred: readonly string[]): AppSpec;
45
+ /**
46
+ * What is wrong with an application's translations, in agentspecs'
47
+ * sentences: its own language, and each translation's, a BCP 47 tag; no
48
+ * translation into its own language, nor into one twice; nothing translated
49
+ * that it does not say.
50
+ */
51
+ export declare function translationProblems(ui: AppInterfaceSpec): string[];
52
+ /** The words the application's interface says itself, in one language. */
53
+ export type InterfaceWords = {
54
+ /** The profile picker's name. */
55
+ profile: string;
56
+ /** Beside the profile a conversation started with: it is kept. */
57
+ profileKept: string;
58
+ /** The paper clip's. */
59
+ attach: string;
60
+ };
61
+ /**
62
+ * The words the application's own interface says — the profile picker, the
63
+ * paper clip — in the languages it is said in. Another language reads
64
+ * English.
65
+ */
66
+ export declare const INTERFACE_WORDS: Record<string, InterfaceWords>;
67
+ /** The interface's own words in a language: its own, else its language's, else English. */
68
+ export declare function interfaceWords(language: string): InterfaceWords;
@@ -0,0 +1,275 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { DEFAULT_LANGUAGE } from './appspec';
6
+ /**
7
+ * A language as BCP 47 tags it: two or three letters, then optionally a
8
+ * script, a region and variants — `fr`, `pt-BR`, `zh-Hant-TW`.
9
+ */
10
+ export const LANGUAGE_TAG = /^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|[0-9]{3}))?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*$/;
11
+ /**
12
+ * The first language a person prefers that is available, or null. Tags are
13
+ * compared without case; a preferred tag also takes one of its language
14
+ * alone, then one of the same language (`fr-CA` takes `fr`, then `fr-FR`).
15
+ */
16
+ export function pickLanguage(available, preferred) {
17
+ const byTag = new Map(available.map(tag => [tag.toLowerCase(), tag]));
18
+ for (const wanted of preferred) {
19
+ const tag = wanted.trim().toLowerCase();
20
+ if (!tag) {
21
+ continue;
22
+ }
23
+ const exact = byTag.get(tag);
24
+ if (exact) {
25
+ return exact;
26
+ }
27
+ const language = tag.split('-')[0];
28
+ const alone = byTag.get(language);
29
+ if (alone) {
30
+ return alone;
31
+ }
32
+ for (const [candidate, original] of byTag) {
33
+ if (candidate.split('-')[0] === language) {
34
+ return original;
35
+ }
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ /** The languages the browser says the person prefers, in their order; none outside one. */
41
+ export function preferredLanguages() {
42
+ if (typeof navigator === 'undefined') {
43
+ return [];
44
+ }
45
+ const languages = navigator.languages?.length
46
+ ? [...navigator.languages]
47
+ : [navigator.language];
48
+ return languages.filter(Boolean);
49
+ }
50
+ /** The language an application is read in by a person who prefers these. */
51
+ export function appLanguage(app, preferred) {
52
+ const own = app.interface.language ?? DEFAULT_LANGUAGE;
53
+ return (pickLanguage([own, ...Object.keys(app.interface.translations ?? {})], preferred) ?? own);
54
+ }
55
+ function translatedStarter(starter, words) {
56
+ const said = words.starters[starter.label];
57
+ const category = starter.category
58
+ ? words.categories[starter.category] || starter.category
59
+ : undefined;
60
+ return {
61
+ label: said?.label || starter.label,
62
+ message: said?.message || starter.message,
63
+ ...(category ? { category } : {}),
64
+ };
65
+ }
66
+ /** The settings' uiSchema, a translated field's values named in the language. */
67
+ function namedOptions(ui, words, settings) {
68
+ const named = Object.entries(words.settings).filter(([, field]) => Object.keys(field.options).length > 0);
69
+ if (named.length === 0 || !settings) {
70
+ return ui;
71
+ }
72
+ const drawn = JSON.parse(JSON.stringify(ui ?? {}));
73
+ for (const [name, field] of named) {
74
+ const schema = (settings.properties?.[name] ?? {});
75
+ const items = (schema.items ?? {});
76
+ const values = (schema.enum ?? items.enum ?? []).map(String);
77
+ drawn[name] = {
78
+ ...(drawn[name] ?? {}),
79
+ 'ui:enumNames': values.map(value => field.options[value] ?? value),
80
+ };
81
+ }
82
+ return drawn;
83
+ }
84
+ /**
85
+ * The application as a person who prefers these languages reads it: its
86
+ * name, description and interface in the first of them it is translated
87
+ * into, else itself. What it does — its agent, its rules, its ids, its
88
+ * values, its prompts — is the same.
89
+ */
90
+ export function translatedAppspec(app, preferred) {
91
+ const tag = appLanguage(app, preferred);
92
+ const words = app.interface.translations?.[tag];
93
+ if (!words) {
94
+ return app;
95
+ }
96
+ const ui = app.interface;
97
+ let settings = ui.settings;
98
+ if (settings && Object.keys(words.settings).length > 0) {
99
+ settings = JSON.parse(JSON.stringify(settings));
100
+ for (const [name, field] of Object.entries(words.settings)) {
101
+ const target = settings.properties?.[name];
102
+ if (!target) {
103
+ continue;
104
+ }
105
+ if (field.title) {
106
+ target.title = field.title;
107
+ }
108
+ if (field.description) {
109
+ target.description = field.description;
110
+ }
111
+ }
112
+ }
113
+ const settingsUi = namedOptions(ui.settingsUi, words, settings);
114
+ return {
115
+ ...app,
116
+ name: words.name || app.name,
117
+ description: words.description || app.description,
118
+ interface: {
119
+ ...ui,
120
+ welcome: words.welcome || ui.welcome,
121
+ starters: ui.starters.map(starter => translatedStarter(starter, words)),
122
+ commands: ui.commands.map(command => ({
123
+ ...command,
124
+ description: words.commands[command.name] || command.description,
125
+ })),
126
+ modes: ui.modes.map(mode => {
127
+ const said = words.modes[mode.id];
128
+ return said
129
+ ? {
130
+ ...mode,
131
+ label: said.label || mode.label,
132
+ options: mode.options.map(option => ({
133
+ ...option,
134
+ label: said.options[option.id]?.label || option.label,
135
+ ...(said.options[option.id]?.description
136
+ ? { description: said.options[option.id].description }
137
+ : {}),
138
+ })),
139
+ }
140
+ : mode;
141
+ }),
142
+ profiles: (ui.profiles ?? []).map(profile => ({
143
+ ...profile,
144
+ label: words.profiles[profile.id]?.label || profile.label,
145
+ description: words.profiles[profile.id]?.description || profile.description,
146
+ starters: profile.starters.map(starter => translatedStarter(starter, words)),
147
+ })),
148
+ ...(settings ? { settings } : {}),
149
+ ...(settingsUi ? { settingsUi } : {}),
150
+ language: tag,
151
+ translations: {},
152
+ },
153
+ };
154
+ }
155
+ /**
156
+ * What is wrong with an application's translations, in agentspecs'
157
+ * sentences: its own language, and each translation's, a BCP 47 tag; no
158
+ * translation into its own language, nor into one twice; nothing translated
159
+ * that it does not say.
160
+ */
161
+ export function translationProblems(ui) {
162
+ const own = ui.language ?? DEFAULT_LANGUAGE;
163
+ const problems = [];
164
+ if (!LANGUAGE_TAG.test(own)) {
165
+ problems.push(`“${own}” is no language as BCP 47 tags it: \`en\`, \`fr\`, \`pt-BR\`.`);
166
+ }
167
+ const profiles = ui.profiles ?? [];
168
+ const starters = new Set([
169
+ ...ui.starters.map(starter => starter.label),
170
+ ...profiles.flatMap(profile => profile.starters.map(item => item.label)),
171
+ ]);
172
+ const categories = new Set([...ui.starters, ...profiles.flatMap(profile => profile.starters)]
173
+ .map(starter => starter.category)
174
+ .filter(Boolean));
175
+ const fields = (ui.settings?.properties ?? {});
176
+ const seen = new Map();
177
+ for (const [tag, words] of Object.entries(ui.translations ?? {})) {
178
+ if (!LANGUAGE_TAG.test(tag)) {
179
+ problems.push(`The translation “${tag}” is of no language as BCP 47 tags it: \`fr\`, \`pt-BR\`.`);
180
+ continue;
181
+ }
182
+ if (tag.toLowerCase() === own.toLowerCase()) {
183
+ problems.push(`Its own words are in “${own}”: no translation into it.`);
184
+ continue;
185
+ }
186
+ const first = seen.get(tag.toLowerCase());
187
+ if (first) {
188
+ problems.push(`“${first}” and “${tag}” are the same language.`);
189
+ continue;
190
+ }
191
+ seen.set(tag.toLowerCase(), tag);
192
+ const unknown = [
193
+ ...Object.keys(words.starters)
194
+ .filter(label => !starters.has(label))
195
+ .map(label => `the starter “${label}”`),
196
+ ...Object.keys(words.categories)
197
+ .filter(name => !categories.has(name))
198
+ .map(name => `the category “${name}”`),
199
+ ...Object.keys(words.settings)
200
+ .filter(name => !(name in fields))
201
+ .map(name => `the setting “${name}”`),
202
+ ...Object.keys(words.commands)
203
+ .filter(name => !ui.commands.some(command => command.name === name))
204
+ .map(name => `the command “${name}”`),
205
+ ...Object.entries(words.modes).flatMap(([id, mode]) => {
206
+ const known = ui.modes.find(item => item.id === id);
207
+ if (!known) {
208
+ return [`the mode “${id}”`];
209
+ }
210
+ return Object.keys(mode.options)
211
+ .filter(option => !known.options.some(item => item.id === option))
212
+ .map(option => `the option “${option}” of “${id}”`);
213
+ }),
214
+ ...Object.keys(words.profiles)
215
+ .filter(id => !profiles.some(profile => profile.id === id))
216
+ .map(id => `the profile “${id}”`),
217
+ ...Object.entries(words.settings).flatMap(([name, field]) => {
218
+ const schema = (fields[name] ?? {});
219
+ const items = (schema.items ?? {});
220
+ const values = (schema.enum ?? items.enum ?? []).map(String);
221
+ return name in fields
222
+ ? Object.keys(field.options)
223
+ .filter(value => !values.includes(value))
224
+ .map(value => `the value “${value}” of “${name}”`)
225
+ : [];
226
+ }),
227
+ ];
228
+ if (unknown.length > 0) {
229
+ problems.push(`The translation “${tag}” translates what the application does not say: ${unknown.join(', ')}.`);
230
+ }
231
+ }
232
+ return problems;
233
+ }
234
+ /**
235
+ * The words the application's own interface says — the profile picker, the
236
+ * paper clip — in the languages it is said in. Another language reads
237
+ * English.
238
+ */
239
+ export const INTERFACE_WORDS = {
240
+ en: {
241
+ profile: 'Profile',
242
+ profileKept: 'kept for this conversation',
243
+ attach: 'Attach a file',
244
+ },
245
+ fr: {
246
+ profile: 'Profil',
247
+ profileKept: 'gardé pour cette conversation',
248
+ attach: 'Joindre un fichier',
249
+ },
250
+ es: {
251
+ profile: 'Perfil',
252
+ profileKept: 'se mantiene en esta conversación',
253
+ attach: 'Adjuntar un archivo',
254
+ },
255
+ de: {
256
+ profile: 'Profil',
257
+ profileKept: 'für dieses Gespräch beibehalten',
258
+ attach: 'Datei anhängen',
259
+ },
260
+ it: {
261
+ profile: 'Profilo',
262
+ profileKept: 'mantenuto per questa conversazione',
263
+ attach: 'Allega un file',
264
+ },
265
+ pt: {
266
+ profile: 'Perfil',
267
+ profileKept: 'mantido nesta conversa',
268
+ attach: 'Anexar um arquivo',
269
+ },
270
+ };
271
+ /** The interface's own words in a language: its own, else its language's, else English. */
272
+ export function interfaceWords(language) {
273
+ const tag = pickLanguage(Object.keys(INTERFACE_WORDS), [language]);
274
+ return INTERFACE_WORDS[tag ?? 'en'];
275
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The nine inputs a setting is drawn with (LOOP P-20): each a JSON Schema
3
+ * field and, where the field's own widget is not the one wanted, the widget a
4
+ * form's uiSchema names for it (`ui:widget`) — what `@datalayer/primer-rjsf`
5
+ * draws. agentspecs' `SETTING_INPUTS`, `FORM_WIDGETS` and
6
+ * `form_ui_problems`, said again for the page, in its sentences.
7
+ *
8
+ * Pure: no React, no network.
9
+ *
10
+ * @module loop/apps/settingsInputs
11
+ */
12
+ /** The nine inputs: the field each is, and the widget it is drawn with. */
13
+ export declare const SETTING_INPUTS: Record<string, {
14
+ field: string;
15
+ widget: string;
16
+ }>;
17
+ /** The widgets a form's `ui` may name (`ui:widget`). */
18
+ export declare const FORM_WIDGETS: readonly ["select", "radio", "range", "updown", "switch", "checkbox", "text", "textarea", "date", "checkboxes", "tags"];
19
+ type Data = Record<string, unknown>;
20
+ /** Why a widget cannot draw a field, in words; null when it can. */
21
+ export declare function widgetRefusal(widget: string, field: Data): string | null;
22
+ /**
23
+ * What is wrong with how a form's fields are drawn, in agentspecs'
24
+ * `form_ui_problems` sentences: by field name, the `ui:` options of each —
25
+ * `ui:widget` one of {@link FORM_WIDGETS} that draws the field — and
26
+ * `ui:order` at its top.
27
+ */
28
+ export declare function formUiProblems(said: string, schema: Data, ui: unknown): string[];
29
+ export {};
@@ -0,0 +1,147 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The nine inputs a setting is drawn with (LOOP P-20): each a JSON Schema
7
+ * field and, where the field's own widget is not the one wanted, the widget a
8
+ * form's uiSchema names for it (`ui:widget`) — what `@datalayer/primer-rjsf`
9
+ * draws. agentspecs' `SETTING_INPUTS`, `FORM_WIDGETS` and
10
+ * `form_ui_problems`, said again for the page, in its sentences.
11
+ *
12
+ * Pure: no React, no network.
13
+ *
14
+ * @module loop/apps/settingsInputs
15
+ */
16
+ /** The nine inputs: the field each is, and the widget it is drawn with. */
17
+ export const SETTING_INPUTS = {
18
+ Select: { field: 'a field with an `enum`', widget: 'select (its own)' },
19
+ Slider: {
20
+ field: 'a `number` or an `integer` with a `minimum` and a `maximum`',
21
+ widget: 'range',
22
+ },
23
+ Switch: { field: 'a `boolean`', widget: 'switch' },
24
+ TextInput: { field: 'a `string`', widget: 'text (its own), or textarea' },
25
+ Checkbox: { field: 'a `boolean`', widget: 'checkbox (its own)' },
26
+ DatePicker: {
27
+ field: 'a `string` of `format: date`',
28
+ widget: 'date (its own)',
29
+ },
30
+ MultiSelect: {
31
+ field: 'an `array` of `uniqueItems` whose `items` have an `enum`',
32
+ widget: 'select (its own), or checkboxes',
33
+ },
34
+ RadioGroup: { field: 'a field with an `enum`', widget: 'radio' },
35
+ Tags: {
36
+ field: 'an `array` of `string` items without an `enum`',
37
+ widget: 'tags',
38
+ },
39
+ };
40
+ /** The widgets a form's `ui` may name (`ui:widget`). */
41
+ export const FORM_WIDGETS = [
42
+ 'select',
43
+ 'radio',
44
+ 'range',
45
+ 'updown',
46
+ 'switch',
47
+ 'checkbox',
48
+ 'text',
49
+ 'textarea',
50
+ 'date',
51
+ 'checkboxes',
52
+ 'tags',
53
+ ];
54
+ const isData = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ /** Why a widget cannot draw a field, in words; null when it can. */
56
+ export function widgetRefusal(widget, field) {
57
+ const kind = field.type;
58
+ const items = isData(field.items) ? field.items : {};
59
+ switch (widget) {
60
+ case 'select':
61
+ case 'radio':
62
+ if ('enum' in field || (widget === 'radio' && kind === 'boolean')) {
63
+ return null;
64
+ }
65
+ if (widget === 'select' && kind === 'array' && 'enum' in items) {
66
+ return null;
67
+ }
68
+ return 'draws a choice: the field has an `enum`';
69
+ case 'range':
70
+ return (kind === 'number' || kind === 'integer') &&
71
+ 'minimum' in field &&
72
+ 'maximum' in field
73
+ ? null
74
+ : 'is a slider: a `number` or an `integer` with a `minimum` and a `maximum`';
75
+ case 'updown':
76
+ return kind === 'number' || kind === 'integer'
77
+ ? null
78
+ : 'draws a `number` or an `integer`';
79
+ case 'switch':
80
+ case 'checkbox':
81
+ return kind === 'boolean' ? null : 'draws a `boolean`';
82
+ case 'text':
83
+ case 'textarea':
84
+ return kind === 'string' ? null : 'draws a `string`';
85
+ case 'date':
86
+ return kind === 'string' ? null : 'draws a `string` of `format: date`';
87
+ case 'checkboxes':
88
+ return kind === 'array' && 'enum' in items
89
+ ? null
90
+ : 'draws an `array` whose `items` have an `enum`';
91
+ case 'tags':
92
+ return kind === 'array' && items.type === 'string' && !('enum' in items)
93
+ ? null
94
+ : 'draws an `array` of `string` items without an `enum`';
95
+ default:
96
+ return `is no widget: ${FORM_WIDGETS.join(', ')}`;
97
+ }
98
+ }
99
+ const quoted = (value) => `“${String(value)}”`;
100
+ /**
101
+ * What is wrong with how a form's fields are drawn, in agentspecs'
102
+ * `form_ui_problems` sentences: by field name, the `ui:` options of each —
103
+ * `ui:widget` one of {@link FORM_WIDGETS} that draws the field — and
104
+ * `ui:order` at its top.
105
+ */
106
+ export function formUiProblems(said, schema, ui) {
107
+ if (!isData(ui)) {
108
+ return [`${said} is drawn by field name: its \`ui\` is a mapping.`];
109
+ }
110
+ const fields = isData(schema.properties) ? schema.properties : {};
111
+ const problems = [];
112
+ for (const [name, options] of Object.entries(ui)) {
113
+ if (name === 'ui:order') {
114
+ const order = Array.isArray(options) ? options : null;
115
+ const unknown = (order ?? []).filter(item => item !== '*' && !(String(item) in fields));
116
+ if (order === null || unknown.length > 0) {
117
+ problems.push(`${said} orders fields it does not ask: ${unknown.map(quoted).join(', ') || String(options)}.`);
118
+ }
119
+ continue;
120
+ }
121
+ if (name.startsWith('ui:')) {
122
+ continue;
123
+ }
124
+ if (!(name in fields)) {
125
+ problems.push(`${said} says how to draw ${quoted(name)}, which it does not ask.`);
126
+ continue;
127
+ }
128
+ if (!isData(options)) {
129
+ problems.push(`${said}'s field ${quoted(name)} is drawn with \`ui:\` options, a mapping.`);
130
+ continue;
131
+ }
132
+ const wrong = Object.keys(options).filter(key => !key.startsWith('ui:') && key !== 'items');
133
+ if (wrong.length > 0) {
134
+ problems.push(`${said}'s field ${quoted(name)} is drawn with \`ui:\` options, not ${wrong.map(quoted).join(', ')}.`);
135
+ }
136
+ const widget = options['ui:widget'];
137
+ const field = fields[name];
138
+ if (widget === undefined || !isData(field)) {
139
+ continue;
140
+ }
141
+ const refusal = widgetRefusal(String(widget), field);
142
+ if (refusal) {
143
+ problems.push(`${said}'s field ${quoted(name)} is drawn with ${quoted(widget)}, which ${refusal}.`);
144
+ }
145
+ }
146
+ return problems;
147
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What a person sends (LOOP P-21), as rules: the kinds of file an
3
+ * application takes in its composer without asking (`interface.uploads`),
4
+ * each with its largest size, and how many at once — and why a file is
5
+ * refused, in the sentence the runtime says when it was sent all the same.
6
+ * agentspecs' own (`AppUploads.refusal`, `too_many`, `upload_kind_takes`),
7
+ * said again for the page.
8
+ *
9
+ * Pure: no React, no network.
10
+ *
11
+ * @module loop/apps/uploads
12
+ */
13
+ import type { AppSpec, AppUploadsSpec } from '../../types/agentspecs';
14
+ /** The largest file a person may send an application, in megabytes. */
15
+ export declare const MAX_UPLOAD_MB = 25;
16
+ /** What a kind takes when it does not say, in megabytes. */
17
+ export declare const DEFAULT_UPLOAD_MB = 10;
18
+ /** How many files go with one message when it does not say. */
19
+ export declare const DEFAULT_MAX_FILES = 5;
20
+ /** A media type, a family of them, or an extension, lowercase. */
21
+ export declare const UPLOAD_KIND: RegExp;
22
+ /** A file as the person gives it: its name, its type, its size in bytes. */
23
+ export type SentFile = {
24
+ name: string;
25
+ type: string;
26
+ size: number;
27
+ };
28
+ /**
29
+ * Whether a kind of file takes a file: an extension (`.csv`) by its name, a
30
+ * media type (`application/pdf`) or a family (`image/*`) by its type.
31
+ */
32
+ export declare function kindTakes(kind: string, file: Pick<SentFile, 'name' | 'type'>): boolean;
33
+ /** Whether any of `kinds` takes a file; any file when there are none. */
34
+ export declare const acceptsFile: (kinds: readonly string[] | undefined, file: Pick<SentFile, "name" | "type">) => boolean;
35
+ /** The `accept` of a file input for what the application takes. */
36
+ export declare const uploadsAccept: (uploads: AppUploadsSpec) => string;
37
+ /**
38
+ * Why files sent without being asked are refused, in a sentence — the
39
+ * runtime's — or `null` when the application takes every one of them.
40
+ */
41
+ export declare function uploadsRefusal(app: Pick<AppSpec, 'name' | 'interface'>, files: readonly SentFile[]): string | null;
42
+ /** What the composer says it takes, in a sentence. */
43
+ export declare function uploadsInWords(uploads: AppUploadsSpec): string;