@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.
- 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 +9 -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 +190 -11
- package/lib/loop/apps/checks.d.ts +6 -0
- package/lib/loop/apps/checks.js +170 -1
- 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.js +2 -0
- 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/apps.js +149 -0
- package/lib/specs/appspecSchema.js +439 -2
- package/lib/specs/uiPlugins.js +6 -1
- package/lib/types/agentspecs.d.ts +130 -1
- package/package.json +1 -1
- 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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
|
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 =
|
|
56
|
-
|
|
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 ??
|
|
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;
|