@formio/uag-js 0.0.0-stage → 2.0.0-rc.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/index.js ADDED
@@ -0,0 +1,1467 @@
1
+ /**
2
+ * @formio/uag-js: the Form.io UAG for formio.js applications.
3
+ *
4
+ * import { Formio } from '@formio/js';
5
+ * import { UAG } from '@formio/uag-js';
6
+ * Formio.use(UAG('https://uag.example.com'));
7
+ * Formio.createForm(element, 'https://forms.example.com/myproject/claim');
8
+ *
9
+ * Registered once, it sends the submission requests of every form tagged `uag`
10
+ * to the UAG (so the form's Run task actions run), and attaches Agent Input to
11
+ * those whose task has an input (`uag: "<task>"`, `uagField: "input"`). Every
12
+ * other request, and every other form, goes to Form.io as before.
13
+ *
14
+ * Agent Input calls the form's own agent operations on the UAG
15
+ * (`{form}/agent/extract`) with the user's Form.io token, applies each engine's
16
+ * results as they stream in (so the form's conditionals react to fast
17
+ * decisions first), and never overwrites a value the user entered or changed.
18
+ *
19
+ * Works as an ES module, a CommonJS module, or the browser global `FormioUAG`.
20
+ */
21
+ (function (root, factory) {
22
+ if (typeof module === 'object' && module.exports) {
23
+ module.exports = factory();
24
+ }
25
+ else {
26
+ root.FormioUAG = factory();
27
+ }
28
+ }(typeof self !== 'undefined' ? self : this, function () {
29
+ 'use strict';
30
+
31
+ function isEmpty(value) {
32
+ if (value === undefined || value === null || value === '' || value === false) return true;
33
+ if (Array.isArray(value)) return value.length === 0;
34
+ if (typeof value === 'object') return !Object.keys(value).some(function (key) { return value[key]; });
35
+ return false;
36
+ }
37
+
38
+ function same(a, b) {
39
+ return JSON.stringify(a) === JSON.stringify(b);
40
+ }
41
+
42
+ /**
43
+ * Where each field's value came from. A value may be written by extraction
44
+ * only when the field is empty, or when its current value is the one
45
+ * extraction wrote last and the user has not touched it since.
46
+ */
47
+ function ExtractionState() {
48
+ this.agentValues = {};
49
+ this.userEdited = {};
50
+ }
51
+ ExtractionState.prototype.canApply = function (path, currentValue) {
52
+ if (this.userEdited[path]) return false;
53
+ if (isEmpty(currentValue)) return true;
54
+ return Object.prototype.hasOwnProperty.call(this.agentValues, path) && same(this.agentValues[path], currentValue);
55
+ };
56
+ ExtractionState.prototype.recordAgent = function (path, value) {
57
+ this.agentValues[path] = value;
58
+ };
59
+ ExtractionState.prototype.recordUser = function (path) {
60
+ this.userEdited[path] = true;
61
+ delete this.agentValues[path];
62
+ };
63
+ ExtractionState.prototype.source = function (path) {
64
+ if (this.userEdited[path]) return 'user';
65
+ return Object.prototype.hasOwnProperty.call(this.agentValues, path) ? 'agent' : 'none';
66
+ };
67
+
68
+ /**
69
+ * Incremental Server-Sent Events parser. Feed it text chunks; it calls
70
+ * `onEvent(name, data)` for each complete event.
71
+ */
72
+ function SSEParser(onEvent) {
73
+ this.buffer = '';
74
+ this.onEvent = onEvent;
75
+ }
76
+ SSEParser.prototype.push = function (chunk) {
77
+ this.buffer += chunk;
78
+ var parts = this.buffer.split('\n\n');
79
+ this.buffer = parts.pop();
80
+ for (var i = 0; i < parts.length; i++) {
81
+ var name = 'message';
82
+ var data = '';
83
+ var lines = parts[i].split('\n');
84
+ for (var j = 0; j < lines.length; j++) {
85
+ if (lines[j].indexOf('event: ') === 0) name = lines[j].slice(7);
86
+ else if (lines[j].indexOf('data: ') === 0) data += lines[j].slice(6);
87
+ }
88
+ if (data) {
89
+ try {
90
+ this.onEvent(name, JSON.parse(data));
91
+ }
92
+ catch (err) {
93
+ // A malformed event is skipped; the final "done" event still completes the request.
94
+ }
95
+ }
96
+ }
97
+ };
98
+
99
+ /** A real DOM, not a server-side shim without createElement. */
100
+ function hasDom() {
101
+ return typeof document !== 'undefined' && typeof document.createElement === 'function';
102
+ }
103
+
104
+ function el(tag, className, text) {
105
+ var node = document.createElement(tag);
106
+ if (className) node.className = className;
107
+ if (text !== undefined) node.textContent = text;
108
+ return node;
109
+ }
110
+
111
+ function pct(value) {
112
+ return Math.round(value * 100) + '%';
113
+ }
114
+
115
+ /** What `UAG()` registered: the UAG's URL, the formio.js it was given, and its options. */
116
+ var registered = { uagUrl: '', Formio: null, options: {} };
117
+
118
+ function trimSlash(url) {
119
+ return String(url || '').replace(/\/+$/, '');
120
+ }
121
+
122
+ /** Whether a form definition is tagged `uag`. */
123
+ function isUagForm(definition) {
124
+ return !!(definition && Array.isArray(definition.tags) && definition.tags.indexOf('uag') !== -1);
125
+ }
126
+
127
+ /**
128
+ * A form's URL on the UAG, by id: under the project's id when the form has
129
+ * one (Enterprise and the hosted service), else at the UAG's root (Open Source).
130
+ */
131
+ function uagFormUrl(uagUrl, definition) {
132
+ if (!uagUrl || !definition || !definition._id) return '';
133
+ var project = definition.project ? '/project/' + encodeURIComponent(String(definition.project)) : '';
134
+ return trimSlash(uagUrl) + project + '/form/' + encodeURIComponent(String(definition._id));
135
+ }
136
+
137
+ /** The user's Form.io token, as formio.js holds it. */
138
+ function formioToken(form) {
139
+ var sdk = registered.Formio;
140
+ try {
141
+ if (form && form.formio && typeof form.formio.getToken === 'function') return form.formio.getToken();
142
+ if (sdk && typeof sdk.getToken === 'function') return sdk.getToken();
143
+ }
144
+ catch (err) {
145
+ return null;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ /** Find the task's input components in a rendered form. */
151
+ function findInputs(form, task) {
152
+ var inputs = [];
153
+ form.everyComponent(function (component) {
154
+ var props = (component.component && component.component.properties) || {};
155
+ if (props.uagField === 'input' && props.uag && (!task || props.uag === task)) {
156
+ inputs.push(component);
157
+ }
158
+ });
159
+ return inputs;
160
+ }
161
+
162
+ function pathOf(component) {
163
+ return component.path || component.key || (component.component && component.component.key);
164
+ }
165
+
166
+ /** The task an input component belongs to (its `uag` property). */
167
+ function taskOfInput(component) {
168
+ var props = (component && component.component && component.component.properties) || {};
169
+ return props.uag || undefined;
170
+ }
171
+
172
+ /**
173
+ * Attach Agent Input to a rendered form. Returns a controller with
174
+ * `extract()` (run now) and `detach()`. Attaching again replaces the form's
175
+ * earlier attachment, so the form never extracts twice.
176
+ */
177
+ function attach(form, options) {
178
+ options = options || {};
179
+ if (form.uagAgent && typeof form.uagAgent.detach === 'function') form.uagAgent.detach();
180
+ var fetchImpl = options.fetch || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
181
+ var state = new ExtractionState();
182
+ var trigger = options.trigger || 'blur';
183
+ var pauseMs = options.pauseMs || 800;
184
+ var nextTimeoutMs = options.nextTimeoutMs || 1500;
185
+ var task = options.task;
186
+ // Where the form's agent operations are: the form on the UAG, or a relay of your own.
187
+ var formUrl = trimSlash(options.formUrl || uagFormUrl(options.uagUrl || registered.uagUrl, form.form));
188
+ var applying = false;
189
+ var lastInputKey = null;
190
+ var inflight = null;
191
+ var timer = null;
192
+ var cleanups = [];
193
+ var annotations = [];
194
+ var noted = {};
195
+ // With `requireReview`, an uncertain value must be checked before the
196
+ // wizard moves on or the form submits: the user changes it, or ticks
197
+ // "Yes, this looks right" on its note.
198
+ var requireReview = options.requireReview === true;
199
+ var reviewed = {};
200
+ var flagged = {};
201
+
202
+ // Remember fields the user changes, so extraction never overwrites them.
203
+ var onChange = function (event, flags, modified) {
204
+ if (applying || !modified || !event || !event.changed) return;
205
+ var instance = event.changed.instance || event.changed.component;
206
+ var path = instance && (instance.path || instance.key);
207
+ if (!path) return;
208
+ state.recordUser(path);
209
+ showReview(path);
210
+ };
211
+ form.on('change', onChange);
212
+ cleanups.push(function () { if (form.off) form.off('change', onChange); });
213
+
214
+ function clearAnnotations() {
215
+ annotations.forEach(function (node) { if (node.parentNode) node.parentNode.removeChild(node); });
216
+ annotations = [];
217
+ noted = {};
218
+ Object.keys(flagged).forEach(unflag);
219
+ reviewed = {};
220
+ }
221
+
222
+ /** Whether an uncertain value has been checked: `changed` by the user, `checked` as right, or `pending`. */
223
+ function reviewStatus(path) {
224
+ if (state.source(path) === 'user') return 'changed';
225
+ return reviewed[path] ? 'checked' : 'pending';
226
+ }
227
+
228
+ /**
229
+ * Show on a field's note whether its value has been checked, and clear
230
+ * its error once it has. The error lives on the note, not in formio.js's
231
+ * validity, which the wizard recomputes (and so clears) after Next.
232
+ */
233
+ function showReview(path) {
234
+ var entry = noted[path];
235
+ if (!requireReview || !entry || entry.result.status !== 'uncertain') return;
236
+ var status = reviewStatus(path);
237
+ if (status !== 'pending') unflag(path);
238
+ if (!entry.node) return;
239
+ var shown = status === 'pending' && flagged[path] ? 'needed' : status;
240
+ entry.node.className = 'uag-chip uag-uncertain' + { pending: '', needed: ' uag-review-needed', checked: ' uag-reviewed', changed: ' uag-reviewed' }[shown];
241
+ entry.text.textContent = {
242
+ pending: 'Please check this. ',
243
+ needed: 'Please check this before you continue. ',
244
+ checked: 'Thanks for checking. ',
245
+ changed: 'You changed this. '
246
+ }[shown];
247
+ // A value the user changed has nothing left to confirm.
248
+ entry.extras.forEach(function (node) { node.hidden = status === 'changed'; });
249
+ }
250
+
251
+ /** Clear the error a field was given for an unchecked value, and the form's alert once none is left. */
252
+ function unflag(path) {
253
+ if (!flagged[path]) return;
254
+ delete flagged[path];
255
+ if (!Object.keys(flagged).length && form.alert && form.setAlert) form.setAlert(false);
256
+ }
257
+
258
+ /** The wizard page a component is on (-1: a page the wizard is not showing); undefined when the form is not a wizard. */
259
+ function pageOf(component) {
260
+ if (!Array.isArray(form.pages)) return undefined;
261
+ for (var node = component; node; node = node.parent) {
262
+ var index = form.pages.indexOf(node);
263
+ if (index !== -1) return index;
264
+ }
265
+ return -1;
266
+ }
267
+
268
+ /** Shown fields whose uncertain value is still to be checked; only those on `page`, when given. */
269
+ function unreviewed(page) {
270
+ return Object.keys(noted).filter(function (path) {
271
+ if (noted[path].result.status !== 'uncertain' || reviewStatus(path) !== 'pending') return false;
272
+ var component = form.getComponent(path);
273
+ if (!component || component.visible === false) return false;
274
+ var index = pageOf(component);
275
+ return index !== -1 && (page === undefined || index === undefined || index === page);
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Stop the wizard or the submission: mark each unchecked field's note,
281
+ * give formio.js a message for its alert, and focus the first checkbox.
282
+ */
283
+ function block(paths, next) {
284
+ next(paths.map(function (path) {
285
+ flagged[path] = true;
286
+ showReview(path);
287
+ var label = form.getComponent(path).component.label;
288
+ return { level: 'error', message: (label ? label + ': ' : '') + 'tick "Yes, this looks right", or change the answer.' };
289
+ }));
290
+ var first = noted[paths[0]];
291
+ if (first.box && first.box.focus) first.box.focus();
292
+ }
293
+
294
+ function annotate(component, result) {
295
+ if (!component) return;
296
+ // Remembered, so a field on a page drawn later is annotated then.
297
+ var path = result.path || pathOf(component);
298
+ var entry = noted[path] = { result: result, node: null, text: null, box: null, extras: [] };
299
+ if (!component.element || !hasDom()) return;
300
+ var note;
301
+ if (result.status === 'confident') {
302
+ note = el('span', 'uag-chip uag-confident', 'Filled from your description' + (result.confidence !== undefined ? ' (' + pct(result.confidence) + ' sure)' : ''));
303
+ }
304
+ else if (result.status === 'uncertain') {
305
+ // An uncertain value can still have a high choice confidence (for
306
+ // example when the input may not address the question at all), so
307
+ // no percentage here; unlikely alternatives are left out.
308
+ if (requireReview) {
309
+ note = el('div', 'uag-chip uag-uncertain');
310
+ entry.text = el('span', 'uag-review-text', 'Please check this. ');
311
+ note.appendChild(entry.text);
312
+ }
313
+ else {
314
+ note = el('div', 'uag-chip uag-uncertain', 'Please check this. ');
315
+ }
316
+ (result.alternatives || []).filter(function (alt) { return !(alt.confidence < 0.05); }).forEach(function (alt) {
317
+ var button = el('button', 'uag-alternative', alt.label + ' ' + pct(alt.confidence));
318
+ button.type = 'button';
319
+ button.addEventListener('click', function () {
320
+ component.setValue(alt.value);
321
+ state.recordUser(path);
322
+ showReview(path);
323
+ });
324
+ note.appendChild(button);
325
+ entry.extras.push(button);
326
+ });
327
+ if (requireReview) {
328
+ var label = el('label', 'uag-review');
329
+ entry.box = el('input');
330
+ entry.box.type = 'checkbox';
331
+ entry.box.checked = !!reviewed[path];
332
+ entry.box.addEventListener('change', function () {
333
+ reviewed[path] = entry.box.checked;
334
+ showReview(path);
335
+ });
336
+ label.appendChild(entry.box);
337
+ label.appendChild(el('span', null, 'Yes, this looks right'));
338
+ note.appendChild(label);
339
+ entry.extras.push(label);
340
+ }
341
+ }
342
+ else if (result.status === 'suggested') {
343
+ note = el('span', 'uag-chip uag-suggested', 'Suggested from your description. Please check it.');
344
+ }
345
+ if (note) {
346
+ component.element.appendChild(note);
347
+ annotations.push(note);
348
+ entry.node = note;
349
+ showReview(path);
350
+ }
351
+ }
352
+
353
+ /** Annotate fields drawn since their results arrived, such as a wizard's later pages. */
354
+ function reannotate() {
355
+ Object.keys(noted).forEach(function (path) {
356
+ var entry = noted[path];
357
+ if (state.source(path) === 'user') return;
358
+ var component = form.getComponent(path);
359
+ if (!component || !component.element) return;
360
+ if (entry.node && component.element.contains && component.element.contains(entry.node)) return;
361
+ annotate(component, entry.result);
362
+ });
363
+ }
364
+ form.on('render', reannotate);
365
+ cleanups.push(function () { if (form.off) form.off('render', reannotate); });
366
+
367
+ function showError(message, forTask) {
368
+ if (!hasDom()) return;
369
+ var anchor = findInputs(form, forTask || task)[0];
370
+ var host = anchor && anchor.element;
371
+ if (!host) return;
372
+ var note = el('div', 'uag-error', message);
373
+ note.setAttribute('role', 'status');
374
+ host.appendChild(note);
375
+ annotations.push(note);
376
+ }
377
+
378
+ /** Apply one engine's results: values the user has not touched, then annotations. */
379
+ function applyResult(event) {
380
+ var fields = event.fields || {};
381
+ var patch = event.patch || {};
382
+ var applied = 0;
383
+ applying = true;
384
+ try {
385
+ Object.keys(patch).forEach(function (path) {
386
+ var component = form.getComponent(path);
387
+ if (!component) return;
388
+ if (!state.canApply(path, component.getValue())) return;
389
+ component.setValue(patch[path]);
390
+ // What the field holds after formio.js normalizes it (select boxes,
391
+ // dates), so it is still recognized as extraction's own value later.
392
+ var written = component.getValue();
393
+ state.recordAgent(path, written === undefined ? patch[path] : written);
394
+ applied++;
395
+ });
396
+ }
397
+ finally {
398
+ applying = false;
399
+ }
400
+ Object.keys(fields).forEach(function (path) {
401
+ var result = fields[path];
402
+ if (result.status === 'missing' || state.source(path) === 'user') return;
403
+ annotate(form.getComponent(path), result);
404
+ });
405
+ if (options.onResult) options.onResult(event);
406
+ return applied;
407
+ }
408
+
409
+ /** The input's current value, to tell when it has changed. */
410
+ function inputKey() {
411
+ return JSON.stringify(inputs.map(function (input) { return input.getValue ? input.getValue() : null; }));
412
+ }
413
+
414
+ /**
415
+ * Empty the values extraction wrote from an earlier input that the user
416
+ * has not changed since, so the new input is answered from scratch.
417
+ */
418
+ function resetAgentValues() {
419
+ applying = true;
420
+ try {
421
+ Object.keys(state.agentValues).forEach(function (path) {
422
+ var component = form.getComponent(path);
423
+ if (component && state.canApply(path, component.getValue())) {
424
+ component.setValue(component.emptyValue !== undefined ? component.emptyValue : '');
425
+ }
426
+ delete state.agentValues[path];
427
+ });
428
+ }
429
+ finally {
430
+ applying = false;
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Call /agent/extract and apply results as they arrive. Resolves when
436
+ * the first decision results have been applied (`decided`) and when
437
+ * the whole request is done (`done`). When the decisions show fields
438
+ * the form was hiding, a follow-up request fills them before `done`.
439
+ * `requestTask` is the task to run: the configured one, else the task
440
+ * of the input that triggered it (none: every task with an input).
441
+ */
442
+ function run(followUp, requestTask) {
443
+ followUp = followUp === true;
444
+ if (inflight && inflight.abort) inflight.abort();
445
+ var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
446
+ inflight = controller;
447
+ if (!followUp) {
448
+ clearAnnotations();
449
+ var key = inputKey();
450
+ if (lastInputKey !== null && key !== lastInputKey) resetAgentValues();
451
+ lastInputKey = key;
452
+ }
453
+ if (options.onStart) options.onStart({ followUp: followUp });
454
+ var decidedValues = 0;
455
+ var summary = null;
456
+ var aborted = false;
457
+ var resolveDecided;
458
+ var decided = new Promise(function (resolve) { resolveDecided = resolve; });
459
+ var headers = { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' };
460
+ var done = Promise.resolve().then(function () {
461
+ if (!fetchImpl) throw new Error('Filling in from your description is unavailable: fetch is not available.');
462
+ if (!formUrl) throw new Error('Filling in from your description is unavailable: the UAG does not know this form.');
463
+ var token = formioToken(form);
464
+ if (token) headers['x-jwt-token'] = token;
465
+ return fetchImpl(formUrl + '/agent/extract', {
466
+ method: 'POST',
467
+ headers: headers,
468
+ body: JSON.stringify({ task: requestTask, data: (form.submission && form.submission.data) || {} }),
469
+ signal: controller ? controller.signal : undefined
470
+ });
471
+ }).then(function (response) {
472
+ if (!response.ok) throw new Error('Filling in from your description is unavailable right now (' + response.status + ').');
473
+ var parser = new SSEParser(function (name, data) {
474
+ if (name === 'result') {
475
+ var applied = applyResult(data);
476
+ if (data.kind === 'decision') {
477
+ decidedValues += applied;
478
+ resolveDecided();
479
+ }
480
+ }
481
+ else if (name === 'done') {
482
+ summary = data;
483
+ }
484
+ else if (name === 'error') {
485
+ // A group whose engines all failed (with its `kind`), or a
486
+ // failure that ended the request (without one).
487
+ var failure = new Error(data.kind
488
+ ? 'Some fields could not be filled from your description. You can fill them in by hand.'
489
+ : (data.error || 'Filling in from your description failed. You can continue by hand.'));
490
+ failure.event = data;
491
+ showError(failure.message, requestTask);
492
+ if (options.onError) options.onError(failure);
493
+ }
494
+ });
495
+ if (response.body && response.body.getReader) {
496
+ var reader = response.body.getReader();
497
+ var decoder = new TextDecoder();
498
+ var pump = function () {
499
+ return reader.read().then(function (chunk) {
500
+ if (chunk.done) return;
501
+ parser.push(decoder.decode(chunk.value, { stream: true }));
502
+ return pump();
503
+ });
504
+ };
505
+ return pump();
506
+ }
507
+ return response.text().then(function (text) { parser.push(text + '\n\n'); });
508
+ }).catch(function (err) {
509
+ if (err && err.name === 'AbortError') {
510
+ aborted = true;
511
+ return;
512
+ }
513
+ showError(err && err.message ? err.message : 'Filling in from your description failed. You can continue by hand.', requestTask);
514
+ if (options.onError) options.onError(err);
515
+ }).then(function () {
516
+ resolveDecided();
517
+ if (inflight === controller) inflight = null;
518
+ if (aborted) return;
519
+ if (options.onDone) options.onDone(summary, { followUp: followUp });
520
+ var revealed = !followUp && decidedValues > 0 && summary && (summary.excluded || []).some(function (entry) {
521
+ return /hidden/.test(entry.reason);
522
+ });
523
+ if (revealed) return run(true, requestTask).done;
524
+ });
525
+ return { decided: decided, done: done };
526
+ }
527
+
528
+ /** Run now, for the configured task (none: every task with an input). */
529
+ function extract() {
530
+ return run(false, task);
531
+ }
532
+
533
+ if (requireReview) {
534
+ // Installed before the triggers, so the `next` trigger's hook runs this
535
+ // one first and extracts only once the page's values are checked.
536
+ form.options = form.options || {};
537
+ var reviewOriginalHooks = form.options.hooks;
538
+ var reviewHooks = Object.assign({}, reviewOriginalHooks);
539
+ var nextBefore = reviewHooks.beforeNext;
540
+ var submitBefore = reviewHooks.beforeSubmit;
541
+ // formio.js calls beforeNext(currentPage, submission, next) on the form.
542
+ reviewHooks.beforeNext = function (currentPage, submission, next) {
543
+ var check = function () {
544
+ var paths = unreviewed(form.page);
545
+ if (paths.length) return block(paths, next);
546
+ next();
547
+ };
548
+ if (nextBefore) return nextBefore.call(this, currentPage, submission, function (err) { if (err) return next(err); check(); });
549
+ check();
550
+ };
551
+ // And beforeSubmit(submission, next), where next takes (err, submission).
552
+ reviewHooks.beforeSubmit = function (submission, next) {
553
+ var check = function (data) {
554
+ var paths = unreviewed();
555
+ if (!paths.length) return next(null, data);
556
+ // Show the first unchecked field, which may be on an earlier page.
557
+ var page = pageOf(form.getComponent(paths[0]));
558
+ if (page === undefined || page === form.page || !form.setPage) return block(paths, next);
559
+ Promise.resolve(form.setPage(page)).then(function () { block(paths, next); });
560
+ };
561
+ if (submitBefore) return submitBefore.call(this, submission, function (err, data) { if (err) return next(err); check(data); });
562
+ check(submission);
563
+ };
564
+ form.options.hooks = reviewHooks;
565
+ cleanups.push(function () { if (form.options.hooks === reviewHooks) form.options.hooks = reviewOriginalHooks; });
566
+ }
567
+
568
+ // Triggers.
569
+ var inputs = findInputs(form, task);
570
+ // formio.js emits `blur` and `change` on the form with the component
571
+ // instance, so the triggers listen there for the task's inputs.
572
+ var inputPaths = inputs.map(pathOf);
573
+ /** The input a component path is, or is inside of. */
574
+ var inputAt = function (path) {
575
+ for (var i = 0; i < inputs.length; i++) {
576
+ if (path === inputPaths[i] || path.indexOf(inputPaths[i] + '.') === 0) return inputs[i];
577
+ }
578
+ return null;
579
+ };
580
+ var isInputPath = function (path) { return !!inputAt(path); };
581
+ var taskAt = function (path) { return task || taskOfInput(inputAt(path)); };
582
+ if (trigger === 'blur') {
583
+ var onBlur = function (instance) {
584
+ if (instance && isInputPath(pathOf(instance))) run(false, taskAt(pathOf(instance)));
585
+ };
586
+ form.on('blur', onBlur);
587
+ cleanups.push(function () { if (form.off) form.off('blur', onBlur); });
588
+ }
589
+ else if (trigger === 'pause') {
590
+ var onTyping = function (event, flags, modified) {
591
+ var instance = event && event.changed && (event.changed.instance || event.changed.component);
592
+ if (!modified || !instance || !isInputPath(pathOf(instance))) return;
593
+ var typedTask = taskAt(pathOf(instance));
594
+ clearTimeout(timer);
595
+ timer = setTimeout(function () { run(false, typedTask); }, pauseMs);
596
+ };
597
+ form.on('change', onTyping);
598
+ cleanups.push(function () { if (form.off) form.off('change', onTyping); });
599
+ }
600
+ else if (trigger === 'next') {
601
+ // Wait for decision results (bounded) before the wizard moves on, so
602
+ // conditional pages reflect the triage.
603
+ form.options = form.options || {};
604
+ // formio.js copies options shallowly, so the hooks object may be shared
605
+ // with other forms: this form gets its own.
606
+ var originalHooks = form.options.hooks;
607
+ var hooks = Object.assign({}, originalHooks);
608
+ var previous = hooks.beforeNext;
609
+ // formio.js calls beforeNext(currentPage, submission, next) on the form.
610
+ hooks.beforeNext = function (currentPage, submission, next) {
611
+ var self = this;
612
+ var advance = function () {
613
+ // The inputs on the page being left; none, when the pages are unknown.
614
+ var onPage = inputs.filter(function (input) {
615
+ return input.page === undefined || form.page === undefined || input.page === form.page;
616
+ });
617
+ if (!onPage.length) return next();
618
+ var pending = run(false, task || taskOfInput(onPage[0]));
619
+ var timeout = new Promise(function (resolve) { setTimeout(resolve, nextTimeoutMs); });
620
+ Promise.race([pending.decided, timeout]).then(function () {
621
+ // formio.js debounces change handling, so conditional pages would
622
+ // not reflect the results yet when the wizard picks its next page.
623
+ if (form.checkData) form.checkData(form.submission.data);
624
+ if (form.establishPages) form.establishPages();
625
+ next();
626
+ });
627
+ };
628
+ if (previous) return previous.call(self, currentPage, submission, function (err) { if (err) return next(err); advance(); });
629
+ advance();
630
+ };
631
+ form.options.hooks = hooks;
632
+ cleanups.push(function () { if (form.options.hooks === hooks) form.options.hooks = originalHooks; });
633
+ }
634
+
635
+ var agent = {
636
+ extract: extract,
637
+ state: state,
638
+ detach: function () {
639
+ clearTimeout(timer);
640
+ if (inflight && inflight.abort) inflight.abort();
641
+ clearAnnotations();
642
+ // Last installed, first removed, so each hook wrapper restores the one it wrapped.
643
+ cleanups.reverse().forEach(function (fn) { fn(); });
644
+ cleanups = [];
645
+ if (form.uagAgent === agent) form.uagAgent = null;
646
+ }
647
+ };
648
+ form.uagAgent = agent;
649
+ return agent;
650
+ }
651
+
652
+ /**
653
+ * The formio.js fetch plugin: it records each form formio.js loads, and
654
+ * sends the submission requests of forms tagged `uag` to the UAG, by id.
655
+ * Nothing else is rerouted, and a request sent to the UAG is never retried
656
+ * at Form.io: the form's server-side tasks must not be skipped.
657
+ */
658
+ function routingPlugin(uagUrl) {
659
+ var forms = {};
660
+ return {
661
+ priority: 0,
662
+ preRequest: function (args) {
663
+ if (!args || !args.formio || (args.type !== 'submission' && args.type !== 'submissions')) return;
664
+ var base = trimSlash(args.formio.formUrl);
665
+ var definition = forms[base];
666
+ if (!definition || !isUagForm(definition) || typeof args.url !== 'string' || args.url.indexOf(base) !== 0) return;
667
+ var rest = args.url.slice(base.length);
668
+ if (rest && rest.charAt(0) !== '/' && rest.charAt(0) !== '?') return;
669
+ args.url = uagFormUrl(uagUrl, definition) + rest;
670
+ },
671
+ wrapRequestPromise: function (promise, args) {
672
+ if (!args || args.type !== 'form' || !args.formio || !promise || typeof promise.then !== 'function') return promise;
673
+ var base = trimSlash(args.formio.formUrl);
674
+ return promise.then(function (definition) {
675
+ // A revision (`{form}/v/{n}`) is its own document: its `_id` is the
676
+ // revision's, and `_rid` the form's, which is where submissions go.
677
+ if (definition && definition._rid) {
678
+ if (!forms[base]) forms[base] = Object.assign({}, definition, { _id: definition._rid });
679
+ }
680
+ else if (definition && definition._id) {
681
+ forms[base] = definition;
682
+ }
683
+ return definition;
684
+ });
685
+ }
686
+ };
687
+ }
688
+
689
+ /**
690
+ * Attach Agent Input to a rendered `uag` form whose task has an input, once.
691
+ * The form must say where it is on the UAG (its `_id`, as Form.io returns
692
+ * it), whether it was loaded from its URL or rendered from a definition.
693
+ */
694
+ function autoAttach(form) {
695
+ var options = registered.options || {};
696
+ if (options.agentInput === false || form.uagAgent) return;
697
+ if (!isUagForm(form.form) || !uagFormUrl(registered.uagUrl, form.form)) return;
698
+ if (!findInputs(form, options.task).length) return;
699
+ form.uagAgent = attach(form, {
700
+ task: options.task,
701
+ trigger: options.trigger || (form.form && form.form.display === 'wizard' ? 'next' : 'blur'),
702
+ pauseMs: options.pauseMs,
703
+ nextTimeoutMs: options.nextTimeoutMs,
704
+ requireReview: options.requireReview,
705
+ fetch: options.fetch,
706
+ onStart: options.onStart,
707
+ onResult: options.onResult,
708
+ onError: options.onError,
709
+ onDone: options.onDone
710
+ });
711
+ }
712
+
713
+ /** Subclasses of a formio.js display that attach Agent Input once rendered. */
714
+ function withAgentInput(Base) {
715
+ return class extends Base {
716
+ attach(element) {
717
+ var self = this;
718
+ return Promise.resolve(super.attach(element)).then(function (result) {
719
+ autoAttach(self);
720
+ return result;
721
+ });
722
+ }
723
+ destroy(all) {
724
+ if (this.uagAgent) {
725
+ this.uagAgent.detach();
726
+ this.uagAgent = null;
727
+ }
728
+ return super.destroy(all);
729
+ }
730
+ };
731
+ }
732
+
733
+ /*
734
+ * The chat: a formio.js display that renders a form as a conversation.
735
+ *
736
+ * Formio.use(UAG('https://uag.example.com'));
737
+ * Formio.createChat(element, 'https://forms.example.com/myproject/servicerequest');
738
+ *
739
+ * Each message the person sends goes to the form's chat on the UAG
740
+ * (`{form}/agent/chat`) with the conversation and the form's data; the
741
+ * values in the reply are written into the form's own components, so its
742
+ * conditionals and validation apply. Nothing is submitted until the person
743
+ * confirms, and then the form submits itself, through the UAG, as any `uag`
744
+ * form does. The real form is one click away, to review or edit.
745
+ */
746
+
747
+ function escapeHtml(value) {
748
+ return String(value === undefined || value === null ? '' : value)
749
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
750
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
751
+ }
752
+
753
+ /** Text as HTML: escaped, with its line breaks kept. */
754
+ function textHtml(text) {
755
+ return escapeHtml(text).replace(/\r?\n/g, '<br>');
756
+ }
757
+
758
+ /**
759
+ * The built-in templates. Each escapes what it interpolates, and uses only
760
+ * `uag-chat-*` classes, so it looks the same under any CSS framework. A
761
+ * template of the same name in `options.templates` or the current framework
762
+ * replaces it; what the chat needs from a template is its `ref` attributes.
763
+ */
764
+ var CHAT_TEMPLATES = {
765
+ uagChat: {
766
+ form: function (ctx) {
767
+ var formView = ctx.view === 'form';
768
+ return '<div class="uag-chat' + (formView ? ' uag-chat--form' : '') + '" ref="uagChat">' +
769
+ '<div class="uag-chat__header">' +
770
+ '<div class="uag-chat__title">' + escapeHtml(ctx.title) + '</div>' +
771
+ '<button type="button" class="uag-chat__action" ref="uagChatOpen"' + (formView ? ' hidden' : '') + '>' + escapeHtml(ctx.labels.open) + '</button>' +
772
+ '<button type="button" class="uag-chat__action" ref="uagChatReset">' + escapeHtml(ctx.labels.reset) + '</button>' +
773
+ '</div>' +
774
+ '<div class="uag-chat__log" ref="uagChatLog" role="log" aria-live="polite" aria-label="' + escapeHtml(ctx.labels.log) + '"' + (formView ? ' hidden' : '') + '>' + ctx.log + '</div>' +
775
+ '<div class="uag-chat__composer" ref="uagChatComposer"' + (formView ? ' hidden' : '') + '>' + ctx.composer + '</div>' +
776
+ '<div class="uag-chat__form" ref="uagChatForm"' + (formView ? '' : ' hidden') + '>' +
777
+ '<div class="uag-chat__form-bar"><button type="button" class="uag-chat__action" ref="uagChatClose">' + escapeHtml(ctx.labels.close) + '</button></div>' +
778
+ ctx.form +
779
+ '</div>' +
780
+ '</div>';
781
+ }
782
+ },
783
+ uagChatMessage: {
784
+ form: function (ctx) {
785
+ return '<div class="uag-chat__message uag-chat__message--' + escapeHtml(ctx.role) + '">' + ctx.html + '</div>';
786
+ }
787
+ },
788
+ uagChatTyping: {
789
+ form: function (ctx) {
790
+ return '<div class="uag-chat__message uag-chat__message--assistant uag-chat__typing" role="status" aria-label="' + escapeHtml(ctx.ariaLabel) + '"><span></span><span></span><span></span></div>';
791
+ }
792
+ },
793
+ uagChatSuggestions: {
794
+ form: function (ctx) {
795
+ return '<div class="uag-chat__suggestions">' + ctx.suggestions.map(function (text, index) {
796
+ return '<button type="button" class="uag-chat__suggestion" ref="uagChatSuggestion" data-index="' + index + '">' + escapeHtml(text) + '</button>';
797
+ }).join('') + '</div>';
798
+ }
799
+ },
800
+ uagChatComposer: {
801
+ form: function (ctx) {
802
+ return '<label class="uag-chat__sr-only" for="' + escapeHtml(ctx.inputId) + '">' + escapeHtml(ctx.inputLabel) + '</label>' +
803
+ '<input id="' + escapeHtml(ctx.inputId) + '" class="uag-chat__input" ref="uagChatInput" type="text" autocomplete="off" maxlength="' + escapeHtml(ctx.maxLength) + '" placeholder="' + escapeHtml(ctx.placeholder) + '"' + (ctx.inactive ? ' disabled' : '') + '>' +
804
+ '<button type="button" class="uag-chat__send" ref="uagChatSend"' + (ctx.inactive || ctx.busy ? ' disabled' : '') + '>' + escapeHtml(ctx.sendLabel) + '</button>';
805
+ }
806
+ },
807
+ uagChatSummary: {
808
+ form: function (ctx) {
809
+ return '<div class="uag-chat__summary">' +
810
+ '<div class="uag-chat__summary-title">' + escapeHtml(ctx.title) + '</div>' +
811
+ '<dl>' + ctx.rows.map(function (row) {
812
+ return '<dt>' + escapeHtml(row.label) + '</dt><dd>' + escapeHtml(row.value) + '</dd>';
813
+ }).join('') + '</dl>' +
814
+ '<div class="uag-chat__summary-actions">' +
815
+ '<button type="button" class="uag-chat__submit" ref="uagChatSubmit"' + (ctx.busy ? ' disabled' : '') + '>' + escapeHtml(ctx.submitLabel) + '</button>' +
816
+ '<button type="button" class="uag-chat__action" ref="uagChatEdit">' + escapeHtml(ctx.editLabel) + '</button>' +
817
+ '</div>' +
818
+ '</div>';
819
+ }
820
+ },
821
+ uagChatNotice: {
822
+ form: function (ctx) {
823
+ return '<div class="uag-chat__notice uag-chat__notice--' + escapeHtml(ctx.kind) + '" role="' + (ctx.kind === 'error' ? 'alert' : 'status') + '">' + escapeHtml(ctx.text) + '</div>';
824
+ }
825
+ }
826
+ };
827
+
828
+ /** The formio.js the chat runs with: the one given to `UAG()`, else the page's global. */
829
+ function formioSdk() {
830
+ if (registered.Formio) return registered.Formio;
831
+ if (typeof window !== 'undefined' && window.Formio) return window.Formio;
832
+ return null;
833
+ }
834
+
835
+ /** Storage for `options.storage`, or null when unavailable (private windows, servers). */
836
+ function chatStorage(kind) {
837
+ try {
838
+ if (kind === 'local' && typeof localStorage !== 'undefined') return localStorage;
839
+ if (kind === 'session' && typeof sessionStorage !== 'undefined') return sessionStorage;
840
+ }
841
+ catch (err) {
842
+ return null;
843
+ }
844
+ return null;
845
+ }
846
+
847
+ /** Who a Form.io token belongs to (its user's id), for keeping conversations apart; `anonymous` without one. */
848
+ function chatUserOf(token) {
849
+ try {
850
+ var payload = token ? JSON.parse(atob(String(token).split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))) : null;
851
+ return (payload && payload.user && payload.user._id) || (token ? 'user' : 'anonymous');
852
+ }
853
+ catch (err) {
854
+ return token ? 'user' : 'anonymous';
855
+ }
856
+ }
857
+
858
+ /** The newest messages a turn may carry, starting at one the person wrote. */
859
+ function recentMessages(messages, max) {
860
+ var start = Math.max(0, messages.length - max);
861
+ while (start < messages.length - 1 && messages[start].role !== 'user') start++;
862
+ return messages.slice(start);
863
+ }
864
+
865
+ /** The `chat` display: a subclass of formio.js's webform display. */
866
+ function chatDisplay(Base, sdk) {
867
+ return class ChatForm extends Base {
868
+ constructor(element, options) {
869
+ super(element, options);
870
+ this.chat = {
871
+ entries: [],
872
+ busy: false,
873
+ conversation: 0,
874
+ view: 'chat',
875
+ complete: false,
876
+ submitted: false,
877
+ response: null,
878
+ unsupportedKey: '',
879
+ restored: false,
880
+ state: new ExtractionState(),
881
+ applying: false
882
+ };
883
+ var self = this;
884
+ // Remember the fields the person changes by hand, so a reply never overwrites them.
885
+ this.on('change', function (event, flags, modified) {
886
+ if (self.chat.applying || !modified || !event || !event.changed) return;
887
+ var instance = event.changed.instance || event.changed.component;
888
+ var path = instance && (instance.path || instance.key);
889
+ if (path) self.chat.state.recordUser(path);
890
+ });
891
+ }
892
+
893
+ /** The messages of the conversation, in order (not the notices or the greeting). */
894
+ get messages() {
895
+ return this.chat.entries.filter(function (entry) { return entry.kind === 'message' && !entry.local; })
896
+ .map(function (entry) { return { role: entry.role, content: entry.text }; });
897
+ }
898
+
899
+ get chatOptions() {
900
+ return this.options || {};
901
+ }
902
+
903
+ /** Whether the form can be used for chat: it is tagged `uag` and the UAG knows it. */
904
+ get chatAvailable() {
905
+ return isUagForm(this.form) && !!this.chatUrl;
906
+ }
907
+
908
+ /** Where turns are sent: `chatUrl`, or the form's chat on the UAG. */
909
+ get chatUrl() {
910
+ if (this.chatOptions.chatUrl) return this.chatOptions.chatUrl;
911
+ var formUrl = uagFormUrl(this.chatOptions.uagUrl || registered.uagUrl, this.form);
912
+ return formUrl ? formUrl + '/agent/chat' : '';
913
+ }
914
+
915
+ /** Per form and per signed-in user, so one person never continues another's conversation. */
916
+ get chatStorageKey() {
917
+ return 'uag-chat:' + (this.chatUrl || (this.form && (this.form._id || this.form.name)) || 'form') + ':' + chatUserOf(this.chatToken());
918
+ }
919
+
920
+ /**
921
+ * Built-in chat templates come after `options.templates` and the
922
+ * current framework's, and before formio.js's own defaults.
923
+ */
924
+ getTemplate(names, modes) {
925
+ var list = Array.isArray(names) ? names : [names];
926
+ var name = list[list.length - 1];
927
+ if (!CHAT_TEMPLATES[name]) return super.getTemplate(names, modes);
928
+ var modeList = Array.isArray(modes) ? modes.slice() : [modes];
929
+ if (modeList.indexOf('form') === -1) modeList.push('form');
930
+ var found = this.options.templates && this.checkTemplate(this.options.templates, list, modeList);
931
+ var Templates = sdk && sdk.Templates;
932
+ var framework = Templates && (this.options.template ? Templates.templates[this.options.template] : Templates.current);
933
+ found = found || (framework && this.checkTemplate(framework, list, modeList));
934
+ return found || { template: CHAT_TEMPLATES[name].form };
935
+ }
936
+
937
+ chatLabels() {
938
+ return {
939
+ open: this.t('Review form'),
940
+ close: this.t('Back to chat'),
941
+ reset: this.t('Start over'),
942
+ log: this.t('Conversation')
943
+ };
944
+ }
945
+
946
+ renderChatMessage(entry) {
947
+ if (entry.kind === 'notice') {
948
+ return this.renderTemplate('uagChatNotice', { kind: entry.level, text: entry.text });
949
+ }
950
+ var html = textHtml(entry.text);
951
+ if (entry.role === 'assistant' && typeof this.chatOptions.markdown === 'function') {
952
+ try {
953
+ html = this.sanitize(String(this.chatOptions.markdown(entry.text) || ''), true);
954
+ }
955
+ catch (err) {
956
+ html = textHtml(entry.text);
957
+ }
958
+ }
959
+ return this.renderTemplate('uagChatMessage', { role: entry.role, text: entry.text, html: html });
960
+ }
961
+
962
+ /** The summary's rows: each visible field with a value, as the form shows it. */
963
+ summaryRows() {
964
+ var rows = [];
965
+ this.everyComponent(function (component) {
966
+ var schema = component.component || {};
967
+ if (!schema.input || schema.type === 'button' || schema.type === 'hidden' || !component.visible) return;
968
+ if (component.hasChildren || (component.components && component.components.length && schema.type !== 'datagrid' && schema.type !== 'editgrid')) return;
969
+ var value = component.dataValue;
970
+ if (isEmpty(value)) return;
971
+ var view;
972
+ try {
973
+ view = component.getView ? component.getView(value) : value;
974
+ }
975
+ catch (err) {
976
+ view = value;
977
+ }
978
+ if (view && typeof view === 'object') view = JSON.stringify(view);
979
+ // getView may return markup for some components; the summary shows text.
980
+ view = String(view === undefined || view === null ? '' : view).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
981
+ rows.push({ label: schema.label || schema.key, value: view });
982
+ });
983
+ return rows;
984
+ }
985
+
986
+ /** The first entry: the greeting, or why the form cannot be used for chat. Not part of the conversation. */
987
+ renderIntro() {
988
+ if (!this.form || !Array.isArray(this.form.components) || !this.form.components.length) return '';
989
+ if (!this.chatAvailable) {
990
+ return this.renderChatMessage({ kind: 'notice', level: 'error', text: this.t('This form is not available for chat.') });
991
+ }
992
+ var greeting = this.chatOptions.greeting !== undefined
993
+ ? this.chatOptions.greeting
994
+ : 'Hi! I can help you fill in {{title}}. What would you like to tell me?';
995
+ return greeting ? this.renderChatMessage({ kind: 'message', role: 'assistant', text: this.t(greeting, { title: this.form.title || '' }) }) : '';
996
+ }
997
+
998
+ renderLog() {
999
+ var self = this;
1000
+ var html = this.renderIntro() + this.chat.entries.map(function (entry) { return self.renderChatMessage(entry); }).join('');
1001
+ var suggestions = (this.chatOptions.suggestions || []).filter(function (text) { return typeof text === 'string' && text; });
1002
+ if (suggestions.length && !this.messages.length && this.chatAvailable) {
1003
+ html += this.renderTemplate('uagChatSuggestions', { suggestions: suggestions.map(function (text) { return self.t(text); }) });
1004
+ }
1005
+ if (this.chat.busy) {
1006
+ html += this.renderTemplate('uagChatTyping', { ariaLabel: this.t('The assistant is typing') });
1007
+ }
1008
+ if (this.chat.complete && !this.chat.submitted && !this.chat.busy) {
1009
+ html += this.renderTemplate('uagChatSummary', {
1010
+ title: this.t('Please check your answers'),
1011
+ rows: this.summaryRows(),
1012
+ submitLabel: this.t(this.chatOptions.submitLabel || 'Submit'),
1013
+ editLabel: this.t('Edit in the form'),
1014
+ busy: this.chat.busy
1015
+ });
1016
+ }
1017
+ return html;
1018
+ }
1019
+
1020
+ renderComposer() {
1021
+ return this.renderTemplate('uagChatComposer', {
1022
+ inputId: this.id + '-uag-chat-input',
1023
+ inputLabel: this.t('Message'),
1024
+ placeholder: this.t(this.chatOptions.placeholder || 'Type your message…'),
1025
+ sendLabel: this.t('Send'),
1026
+ maxLength: this.chatOptions.maxMessageLength || 2000,
1027
+ // Not `disabled`: formio.js sets that on every template's context.
1028
+ inactive: !this.chatAvailable || this.chat.submitted,
1029
+ busy: this.chat.busy
1030
+ });
1031
+ }
1032
+
1033
+ render() {
1034
+ return this.renderTemplate('uagChat', {
1035
+ title: this.t(this.chatOptions.title || (this.form && this.form.title) || ''),
1036
+ view: this.chat.view,
1037
+ labels: this.chatLabels(),
1038
+ log: this.renderLog(),
1039
+ composer: this.renderComposer(),
1040
+ form: super.render()
1041
+ });
1042
+ }
1043
+
1044
+ attach(element) {
1045
+ var self = this;
1046
+ var attached = super.attach(element);
1047
+ this.loadRefs(element, {
1048
+ uagChat: 'single',
1049
+ uagChatLog: 'single',
1050
+ uagChatComposer: 'single',
1051
+ uagChatInput: 'single',
1052
+ uagChatSend: 'single',
1053
+ uagChatForm: 'single',
1054
+ uagChatOpen: 'single',
1055
+ uagChatClose: 'single',
1056
+ uagChatReset: 'single'
1057
+ });
1058
+ this.attachChatControls();
1059
+ return Promise.resolve(attached).then(function (result) {
1060
+ self.restoreChat();
1061
+ self.scrollChat();
1062
+ return result;
1063
+ });
1064
+ }
1065
+
1066
+ attachChatControls() {
1067
+ var self = this;
1068
+ var refs = this.refs;
1069
+ if (refs.uagChatSend) {
1070
+ this.addEventListener(refs.uagChatSend, 'click', function () { self.sendFromInput(); });
1071
+ }
1072
+ if (refs.uagChatInput) {
1073
+ this.addEventListener(refs.uagChatInput, 'keydown', function (event) {
1074
+ if (event.key === 'Enter' && !event.shiftKey) {
1075
+ event.preventDefault();
1076
+ self.sendFromInput();
1077
+ }
1078
+ });
1079
+ }
1080
+ if (refs.uagChatOpen) this.addEventListener(refs.uagChatOpen, 'click', function () { self.openForm(); });
1081
+ if (refs.uagChatClose) this.addEventListener(refs.uagChatClose, 'click', function () { self.closeForm(); });
1082
+ if (refs.uagChatReset) this.addEventListener(refs.uagChatReset, 'click', function () { self.reset(); });
1083
+ // The log is redrawn often: its buttons are handled where they land.
1084
+ if (refs.uagChatLog) {
1085
+ this.addEventListener(refs.uagChatLog, 'click', function (event) {
1086
+ var target = event.target && event.target.closest ? event.target.closest('[ref]') : null;
1087
+ if (!target || !refs.uagChatLog.contains(target)) return;
1088
+ var ref = target.getAttribute('ref');
1089
+ if (ref === 'uagChatSuggestion') self.send(target.textContent || '');
1090
+ else if (ref === 'uagChatSubmit') self.submitChat();
1091
+ else if (ref === 'uagChatEdit') self.openForm();
1092
+ });
1093
+ }
1094
+ }
1095
+
1096
+ sendFromInput() {
1097
+ var input = this.refs.uagChatInput;
1098
+ if (!input || this.chat.busy) return;
1099
+ var text = input.value;
1100
+ if (!String(text || '').trim()) return;
1101
+ input.value = '';
1102
+ this.send(text);
1103
+ }
1104
+
1105
+ scrollChat() {
1106
+ var log = this.refs && this.refs.uagChatLog;
1107
+ if (log) log.scrollTop = log.scrollHeight;
1108
+ }
1109
+
1110
+ /** Redraw the log and the composer from the chat's state. */
1111
+ updateChat() {
1112
+ if (!this.refs || !this.refs.uagChatLog) return;
1113
+ this.refs.uagChatLog.innerHTML = this.sanitize(this.renderLog(), true);
1114
+ if (this.refs.uagChatComposer) {
1115
+ var focused = typeof document !== 'undefined' && document.activeElement === this.refs.uagChatInput;
1116
+ var typed = this.refs.uagChatInput ? this.refs.uagChatInput.value : '';
1117
+ this.refs.uagChatComposer.innerHTML = this.sanitize(this.renderComposer(), true);
1118
+ this.loadRefs(this.refs.uagChatComposer, { uagChatInput: 'single', uagChatSend: 'single' });
1119
+ var self = this;
1120
+ if (this.refs.uagChatSend) this.addEventListener(this.refs.uagChatSend, 'click', function () { self.sendFromInput(); });
1121
+ if (this.refs.uagChatInput) {
1122
+ this.refs.uagChatInput.value = typed;
1123
+ this.addEventListener(this.refs.uagChatInput, 'keydown', function (event) {
1124
+ if (event.key === 'Enter' && !event.shiftKey) {
1125
+ event.preventDefault();
1126
+ self.sendFromInput();
1127
+ }
1128
+ });
1129
+ if (focused && !this.refs.uagChatInput.disabled) this.refs.uagChatInput.focus();
1130
+ }
1131
+ }
1132
+ this.scrollChat();
1133
+ }
1134
+
1135
+ addChatEntry(entry) {
1136
+ this.chat.entries.push(entry);
1137
+ if (entry.kind === 'message') this.emit('chatMessage', { role: entry.role, content: entry.text });
1138
+ }
1139
+
1140
+ notice(level, text) {
1141
+ this.addChatEntry({ kind: 'notice', level: level, text: text });
1142
+ this.updateChat();
1143
+ }
1144
+
1145
+ setView(view) {
1146
+ this.chat.view = view;
1147
+ var refs = this.refs || {};
1148
+ var formView = view === 'form';
1149
+ if (refs.uagChat) refs.uagChat.classList.toggle('uag-chat--form', formView);
1150
+ [refs.uagChatLog, refs.uagChatComposer, refs.uagChatOpen].forEach(function (node) { if (node) node.hidden = formView; });
1151
+ if (refs.uagChatForm) refs.uagChatForm.hidden = !formView;
1152
+ if (!formView) this.updateChat();
1153
+ }
1154
+
1155
+ openForm() {
1156
+ this.setView('form');
1157
+ }
1158
+
1159
+ closeForm() {
1160
+ this.setView('chat');
1161
+ }
1162
+
1163
+ /** The person's Form.io token, as formio.js holds it. */
1164
+ chatToken() {
1165
+ return formioToken(this);
1166
+ }
1167
+
1168
+ /**
1169
+ * Send a message: post the conversation and the form's data to the
1170
+ * form's chat, then show the reply and apply its values. Resolves
1171
+ * when the turn is over; failures are shown, never thrown.
1172
+ */
1173
+ send(text) {
1174
+ var self = this;
1175
+ text = String(text || '').trim();
1176
+ if (!text || this.chat.busy || this.chat.submitted || !this.chatAvailable) return Promise.resolve(null);
1177
+ var fetchImpl = this.chatOptions.fetch || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
1178
+ var conversation = this.chat.conversation;
1179
+ this.addChatEntry({ kind: 'message', role: 'user', text: text });
1180
+ this.chat.busy = true;
1181
+ this.updateChat();
1182
+ var headers = { 'Content-Type': 'application/json' };
1183
+ var token = this.chatToken();
1184
+ if (token) headers['x-jwt-token'] = token;
1185
+ var body = {
1186
+ messages: recentMessages(this.messages, this.chatOptions.maxMessages || 40),
1187
+ // What the person set in the form itself: the chat never changes it.
1188
+ locked: Object.keys(this.chat.state.userEdited),
1189
+ data: (this.submission && this.submission.data) || {}
1190
+ };
1191
+ return Promise.resolve().then(function () {
1192
+ if (!fetchImpl) throw new Error(self.t('The chat is unavailable: fetch is not available.'));
1193
+ return fetchImpl(self.chatUrl, { method: 'POST', headers: headers, body: JSON.stringify(body) });
1194
+ }).then(function (response) {
1195
+ return response.text().then(function (raw) {
1196
+ var payload = null;
1197
+ try {
1198
+ payload = raw ? JSON.parse(raw) : null;
1199
+ }
1200
+ catch (err) {
1201
+ payload = null;
1202
+ }
1203
+ if (!response.ok) {
1204
+ var failure = new Error((payload && (payload.error || payload.detail)) || self.t('The assistant could not answer ({{status}}). Please try again.', { status: response.status }));
1205
+ failure.status = response.status;
1206
+ failure.response = payload;
1207
+ throw failure;
1208
+ }
1209
+ return payload;
1210
+ });
1211
+ }).then(function (payload) {
1212
+ if (conversation !== self.chat.conversation) return null;
1213
+ self.chat.busy = false;
1214
+ self.applyTurn(payload || {});
1215
+ return payload;
1216
+ }).catch(function (err) {
1217
+ if (conversation !== self.chat.conversation) return null;
1218
+ self.chat.busy = false;
1219
+ self.notice('error', (err && err.message) || self.t('The assistant could not answer. Please try again.'));
1220
+ self.emit('chatError', err);
1221
+ return null;
1222
+ });
1223
+ }
1224
+
1225
+ /** Show a turn's reply, apply its values, and move on when the form is ready. */
1226
+ applyTurn(response) {
1227
+ var self = this;
1228
+ var wasComplete = this.chat.complete;
1229
+ this.chat.response = response;
1230
+ if (response.reply) this.addChatEntry({ kind: 'message', role: 'assistant', text: String(response.reply) });
1231
+ this.applyPatch(response.patch || {});
1232
+ this.chat.complete = !!response.complete;
1233
+ this.emit('chatReply', response);
1234
+ var unsupported = ((response.remaining && response.remaining.unsupported) || []).filter(function (field) { return field.required && field.empty; });
1235
+ // Said once for each set of fields, not on every reply while they stay empty.
1236
+ var unsupportedKey = unsupported.map(function (field) { return field.path; }).join(',');
1237
+ var newlyUnsupported = !!unsupported.length && unsupportedKey !== this.chat.unsupportedKey;
1238
+ this.chat.unsupportedKey = unsupportedKey;
1239
+ if (newlyUnsupported) {
1240
+ this.addChatEntry({
1241
+ kind: 'notice',
1242
+ level: 'info',
1243
+ text: this.t('Please finish these in the form: {{fields}}', { fields: unsupported.map(function (field) { return field.label; }).join(', ') })
1244
+ });
1245
+ }
1246
+ this.saveChat();
1247
+ this.updateChat();
1248
+ if (newlyUnsupported) this.openForm();
1249
+ if (this.chat.complete && !wasComplete) this.emit('chatComplete', response);
1250
+ if (response.confirmed && this.chat.complete) return this.submitChat();
1251
+ return Promise.resolve(response);
1252
+ }
1253
+
1254
+ /** Write a turn's values into the form, never over the person's own. */
1255
+ applyPatch(patch) {
1256
+ var self = this;
1257
+ this.chat.applying = true;
1258
+ try {
1259
+ Object.keys(patch).forEach(function (path) {
1260
+ var component = self.getComponent(path);
1261
+ if (!component) return;
1262
+ if (!self.chat.state.canApply(path, component.getValue())) return;
1263
+ component.setValue(patch[path]);
1264
+ var written = component.getValue();
1265
+ self.chat.state.recordAgent(path, written === undefined ? patch[path] : written);
1266
+ });
1267
+ }
1268
+ finally {
1269
+ this.chat.applying = false;
1270
+ }
1271
+ }
1272
+
1273
+ /** Submit the form through formio.js (so through the UAG), and say how it went. */
1274
+ submitChat() {
1275
+ var self = this;
1276
+ if (this.chat.submitted || this.chat.busy) return Promise.resolve(null);
1277
+ var conversation = this.chat.conversation;
1278
+ this.chat.busy = true;
1279
+ this.updateChat();
1280
+ return Promise.resolve().then(function () {
1281
+ return self.submit();
1282
+ }).then(function (submission) {
1283
+ // Started over while it was saving: the new conversation is not the one submitted.
1284
+ if (conversation !== self.chat.conversation) return submission;
1285
+ self.chat.busy = false;
1286
+ self.chat.submitted = true;
1287
+ self.addChatEntry({ kind: 'message', role: 'assistant', local: true, text: self.t(self.chatOptions.submitted || 'Thanks! Your form was submitted.') });
1288
+ self.clearChatStorage();
1289
+ self.updateChat();
1290
+ return submission;
1291
+ }).catch(function (err) {
1292
+ if (conversation !== self.chat.conversation) return null;
1293
+ self.chat.busy = false;
1294
+ var errors = Array.isArray(err) ? err : [err];
1295
+ var text = errors.map(function (item) {
1296
+ if (typeof item === 'string') return item;
1297
+ return (item && (item.message || item.error || (item.details && item.details[0] && item.details[0].message))) || '';
1298
+ }).filter(Boolean).join(' ') || self.t('The form could not be submitted.');
1299
+ self.notice('error', text);
1300
+ self.emit('chatError', err);
1301
+ return null;
1302
+ });
1303
+ }
1304
+
1305
+ /** Start the conversation again, clearing the values the chat wrote. */
1306
+ reset() {
1307
+ var self = this;
1308
+ var wasSubmitted = this.chat.submitted;
1309
+ this.chat.conversation++;
1310
+ this.chat.busy = false;
1311
+ this.chat.complete = false;
1312
+ this.chat.submitted = false;
1313
+ this.chat.response = null;
1314
+ this.chat.entries = [];
1315
+ this.chat.unsupportedKey = '';
1316
+ this.chat.applying = true;
1317
+ try {
1318
+ if (wasSubmitted) {
1319
+ // A new request starts from an empty form, not as an update of the one just saved.
1320
+ this.resetValue();
1321
+ this._submission = { data: this.data };
1322
+ }
1323
+ Object.keys(this.chat.state.agentValues).forEach(function (path) {
1324
+ var component = self.getComponent(path);
1325
+ if (component && self.chat.state.canApply(path, component.getValue())) {
1326
+ component.setValue(component.emptyValue !== undefined ? component.emptyValue : '');
1327
+ }
1328
+ });
1329
+ }
1330
+ finally {
1331
+ this.chat.applying = false;
1332
+ }
1333
+ this.chat.state = new ExtractionState();
1334
+ this.clearChatStorage();
1335
+ this.setView('chat');
1336
+ this.updateChat();
1337
+ this.emit('chatReset');
1338
+ }
1339
+
1340
+ saveChat() {
1341
+ var storage = chatStorage(this.chatOptions.storage);
1342
+ if (!storage) return;
1343
+ try {
1344
+ storage.setItem(this.chatStorageKey, JSON.stringify({
1345
+ entries: this.chat.entries,
1346
+ data: (this.submission && this.submission.data) || {},
1347
+ agentValues: this.chat.state.agentValues,
1348
+ userEdited: this.chat.state.userEdited,
1349
+ complete: this.chat.complete
1350
+ }));
1351
+ }
1352
+ catch (err) {
1353
+ // A full or blocked storage only loses the continuation.
1354
+ }
1355
+ }
1356
+
1357
+ clearChatStorage() {
1358
+ var storage = chatStorage(this.chatOptions.storage);
1359
+ try {
1360
+ if (storage) storage.removeItem(this.chatStorageKey);
1361
+ }
1362
+ catch (err) {
1363
+ // Nothing to clear.
1364
+ }
1365
+ }
1366
+
1367
+ /** Continue a conversation kept with `options.storage`, once. */
1368
+ restoreChat() {
1369
+ if (this.chat.restored) return;
1370
+ this.chat.restored = true;
1371
+ // A submission loaded from its URL is what the form shows; a kept conversation does not replace it.
1372
+ if (this.submission && this.submission._id) return;
1373
+ var storage = chatStorage(this.chatOptions.storage);
1374
+ var saved = null;
1375
+ try {
1376
+ saved = storage ? JSON.parse(storage.getItem(this.chatStorageKey) || 'null') : null;
1377
+ }
1378
+ catch (err) {
1379
+ saved = null;
1380
+ }
1381
+ if (!saved || !Array.isArray(saved.entries)) return;
1382
+ this.chat.entries = saved.entries.filter(function (entry) { return entry && typeof entry.text === 'string'; });
1383
+ this.chat.state.agentValues = saved.agentValues || {};
1384
+ this.chat.state.userEdited = saved.userEdited || {};
1385
+ this.chat.complete = !!saved.complete;
1386
+ this.chat.applying = true;
1387
+ try {
1388
+ this.setValue({ data: saved.data || {} });
1389
+ }
1390
+ finally {
1391
+ this.chat.applying = false;
1392
+ }
1393
+ this.updateChat();
1394
+ }
1395
+ };
1396
+ }
1397
+
1398
+ var chatClasses = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
1399
+
1400
+ /** The `chat` display for this formio.js, built once on its original webform display. */
1401
+ function chatDisplayFor(sdk, Base) {
1402
+ var Webform = Base || (sdk && sdk.Displays && sdk.Displays.displays && sdk.Displays.displays.webform);
1403
+ if (!Webform) throw new Error('createChat(): formio.js 5 is required.');
1404
+ var cached = chatClasses && chatClasses.get(Webform);
1405
+ if (cached) return cached;
1406
+ var ChatForm = chatDisplay(Webform, sdk);
1407
+ if (chatClasses) chatClasses.set(Webform, ChatForm);
1408
+ return ChatForm;
1409
+ }
1410
+
1411
+ /**
1412
+ * Render a form as a chat, as `Formio.createForm` renders it as a form.
1413
+ * `src` is the form's Form.io URL (or a definition with its `_id`); the
1414
+ * promise resolves to the chat, a formio.js form with chat methods.
1415
+ */
1416
+ function createChat(element, src, options) {
1417
+ var sdk = formioSdk();
1418
+ if (!sdk || !sdk.Form || !sdk.Displays) {
1419
+ return Promise.reject(new Error('createChat(): load formio.js 5 and register the UAG first: Formio.use(UAG(\'https://uag.example.com\')).'));
1420
+ }
1421
+ if (!sdk.Displays.displays.chat) sdk.Displays.addDisplay('chat', chatDisplayFor(sdk));
1422
+ var wrapper = new sdk.Form(element, undefined, Object.assign({}, options || {}));
1423
+ wrapper.instance = wrapper.create('chat');
1424
+ return wrapper.setForm(src);
1425
+ }
1426
+
1427
+ /**
1428
+ * The formio.js module: `Formio.use(UAG('https://uag.example.com', options))`.
1429
+ * Options: `agentInput` (`false` turns Agent Input off), and Agent Input's
1430
+ * `task`, `trigger`, `pauseMs`, `nextTimeoutMs` and callbacks. It also
1431
+ * registers the `chat` display, and `Formio.createChat`.
1432
+ */
1433
+ function UAG(uagUrl, options) {
1434
+ if (!uagUrl) throw new Error('UAG(): give the UAG\'s URL, e.g. UAG(\'https://uag.example.com\').');
1435
+ return function (Formio) {
1436
+ registered = { uagUrl: trimSlash(uagUrl), Formio: Formio || null, options: options || {} };
1437
+ var mod = { fetch: { uag: routingPlugin(registered.uagUrl) } };
1438
+ var displays = (Formio && Formio.Displays && Formio.Displays.displays) || {};
1439
+ mod.displays = {};
1440
+ // The chat builds on the original webform display, never on Agent Input's.
1441
+ if (displays.webform) mod.displays.chat = chatDisplayFor(Formio, displays.webform);
1442
+ if ((options || {}).agentInput !== false) {
1443
+ ['webform', 'wizard'].forEach(function (name) {
1444
+ if (displays[name]) mod.displays[name] = withAgentInput(displays[name]);
1445
+ });
1446
+ }
1447
+ if (Formio && typeof Formio.createChat !== 'function') {
1448
+ Formio.createChat = createChat;
1449
+ }
1450
+ return mod;
1451
+ };
1452
+ }
1453
+ UAG.attach = attach;
1454
+ UAG.createChat = createChat;
1455
+
1456
+ return {
1457
+ UAG: UAG,
1458
+ attach: attach,
1459
+ createChat: createChat,
1460
+ uagFormUrl: uagFormUrl,
1461
+ routingPlugin: routingPlugin,
1462
+ ExtractionState: ExtractionState,
1463
+ SSEParser: SSEParser,
1464
+ isEmpty: isEmpty,
1465
+ CHAT_TEMPLATES: CHAT_TEMPLATES
1466
+ };
1467
+ }));