veform-js 0.1.0
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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.js +1 -0
- package/dist/veform-builder.d.ts +118 -0
- package/dist/veform-builder.js +184 -0
- package/dist/veform.browser.js +514 -0
- package/dist/veform.browser.min.js +1 -0
- package/dist/veform.d.ts +102 -0
- package/dist/veform.js +305 -0
- package/package.json +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eric McElyea
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
File without changes
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
declare enum FieldType {
|
|
2
|
+
TEXT = "text",
|
|
3
|
+
TEXTAREA = "textarea",
|
|
4
|
+
SELECT = "select",
|
|
5
|
+
MULTISELECT = "multiselect",
|
|
6
|
+
YESNO = "yesNo",
|
|
7
|
+
NUMBER = "number",
|
|
8
|
+
INFO = "info"
|
|
9
|
+
}
|
|
10
|
+
type EventHandlers = {
|
|
11
|
+
onFocus?: (previousName: string) => boolean;
|
|
12
|
+
onChange?: (answer: string | number | boolean) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare abstract class Field {
|
|
15
|
+
name: string;
|
|
16
|
+
question: string;
|
|
17
|
+
type: FieldType;
|
|
18
|
+
eventConfig?: FieldEventConfig | undefined;
|
|
19
|
+
eventHandlers: EventHandlers;
|
|
20
|
+
constructor(name: string, question: string, type: FieldType, eventConfig?: FieldEventConfig | undefined, eventHandlers?: EventHandlers);
|
|
21
|
+
addBehavior(event: FieldEvents, behavior: FieldBehavior): void;
|
|
22
|
+
onFocus(callback: EventHandlers['onFocus']): void;
|
|
23
|
+
onChange(callback: EventHandlers['onChange']): void;
|
|
24
|
+
}
|
|
25
|
+
export type FieldEventConfig = {
|
|
26
|
+
[key in FieldEvents]?: FieldBehavior[];
|
|
27
|
+
};
|
|
28
|
+
declare enum FieldEvents {
|
|
29
|
+
VALID_ANSWER = "validAnswer",
|
|
30
|
+
INVALID_ANSWER = "invalidAnswer",
|
|
31
|
+
MOVE_REQUESTED = "moveRequested",
|
|
32
|
+
END_REQUESTED = "endRequested",
|
|
33
|
+
VALID_YES_ANSWER = "validYesAnswer",
|
|
34
|
+
VALID_NO_ANSWER = "validNoAnswer"
|
|
35
|
+
}
|
|
36
|
+
declare enum FieldBehaviorType {
|
|
37
|
+
MOVE_TO = "moveTo",
|
|
38
|
+
OUTPUT = "output"
|
|
39
|
+
}
|
|
40
|
+
type FieldBehavior = {
|
|
41
|
+
type: FieldBehaviorType;
|
|
42
|
+
moveToFieldName: string;
|
|
43
|
+
output?: string;
|
|
44
|
+
modifier?: string;
|
|
45
|
+
};
|
|
46
|
+
export declare class TextField extends Field {
|
|
47
|
+
textFieldValidation?: TextFieldValidation;
|
|
48
|
+
constructor(name: string, question: string, eventConfig?: FieldEventConfig);
|
|
49
|
+
addValidation(validation: TextFieldValidation): void;
|
|
50
|
+
}
|
|
51
|
+
type TextFieldPatterns = 'email' | 'phone' | 'url' | 'date' | 'name';
|
|
52
|
+
type TextFieldValidation = {
|
|
53
|
+
validate: boolean;
|
|
54
|
+
pattern?: TextFieldPatterns;
|
|
55
|
+
readback?: boolean;
|
|
56
|
+
};
|
|
57
|
+
export declare class NumberField extends Field {
|
|
58
|
+
numberFieldValidation?: NumberFieldValidation;
|
|
59
|
+
constructor(name: string, question: string, eventConfig?: FieldEventConfig);
|
|
60
|
+
addValidation(validation: NumberFieldValidation): void;
|
|
61
|
+
}
|
|
62
|
+
type NumberFieldValidation = {
|
|
63
|
+
validate: boolean;
|
|
64
|
+
minValue?: number;
|
|
65
|
+
maxValue?: number;
|
|
66
|
+
};
|
|
67
|
+
export declare class SelectField extends Field {
|
|
68
|
+
selectFieldValidation?: SelectFieldValidation;
|
|
69
|
+
constructor(name: string, question: string, eventConfig?: FieldEventConfig);
|
|
70
|
+
addSelectOption(option: SelectOption): void;
|
|
71
|
+
}
|
|
72
|
+
export type SelectOption = {
|
|
73
|
+
label: string;
|
|
74
|
+
value: string;
|
|
75
|
+
readAloud?: boolean;
|
|
76
|
+
behaviors?: FieldBehavior[];
|
|
77
|
+
};
|
|
78
|
+
type SelectFieldValidation = {
|
|
79
|
+
validate: boolean;
|
|
80
|
+
selectOptions?: SelectOption[];
|
|
81
|
+
};
|
|
82
|
+
export declare class MultiselectField extends Field {
|
|
83
|
+
multiselectFieldValidation?: SelectFieldValidation;
|
|
84
|
+
constructor(name: string, question: string, eventConfig?: FieldEventConfig);
|
|
85
|
+
addSelectOption(option: SelectOption): void;
|
|
86
|
+
}
|
|
87
|
+
export declare class YesNoField extends Field {
|
|
88
|
+
yesNoFieldValidation?: YesNoFieldValidation;
|
|
89
|
+
constructor(name: string, question: string, eventConfig?: FieldEventConfig);
|
|
90
|
+
addValidation(validation: YesNoFieldValidation): void;
|
|
91
|
+
}
|
|
92
|
+
type YesNoFieldValidation = {
|
|
93
|
+
validate: boolean;
|
|
94
|
+
requireYes?: boolean;
|
|
95
|
+
requireNo?: boolean;
|
|
96
|
+
};
|
|
97
|
+
export declare class TextAreaField extends Field {
|
|
98
|
+
textAreaFieldValidation?: TextAreaFieldValidation;
|
|
99
|
+
constructor(name: string, question: string, eventConfig?: FieldEventConfig);
|
|
100
|
+
addValidation(validation: TextAreaFieldValidation): void;
|
|
101
|
+
}
|
|
102
|
+
type TextAreaFieldValidation = {
|
|
103
|
+
validate: boolean;
|
|
104
|
+
maxCharacters?: number;
|
|
105
|
+
minCharacters?: number;
|
|
106
|
+
};
|
|
107
|
+
export declare class InfoField extends Field {
|
|
108
|
+
constructor(name: string, question: string, eventConfig?: FieldEventConfig);
|
|
109
|
+
}
|
|
110
|
+
export declare class VeformBuilder {
|
|
111
|
+
private fields;
|
|
112
|
+
addField({ name, question, type }: Field): Field | null;
|
|
113
|
+
getField(name: string): Field | undefined;
|
|
114
|
+
getFields(): Field[];
|
|
115
|
+
setField(name: string, field: Field): boolean;
|
|
116
|
+
removeField(name: string): boolean;
|
|
117
|
+
}
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// TMRW MOVE THESE ALL TO CLASSES THAT CAN CONVERT TO THE JSON PAYLOAD WHEN NEEDED
|
|
2
|
+
// then test we can actuall parse this nonsense in GO
|
|
3
|
+
var FieldType;
|
|
4
|
+
(function (FieldType) {
|
|
5
|
+
FieldType["TEXT"] = "text";
|
|
6
|
+
FieldType["TEXTAREA"] = "textarea";
|
|
7
|
+
FieldType["SELECT"] = "select";
|
|
8
|
+
FieldType["MULTISELECT"] = "multiselect";
|
|
9
|
+
FieldType["YESNO"] = "yesNo";
|
|
10
|
+
FieldType["NUMBER"] = "number";
|
|
11
|
+
FieldType["INFO"] = "info";
|
|
12
|
+
})(FieldType || (FieldType = {}));
|
|
13
|
+
export class Field {
|
|
14
|
+
constructor(name, question, type, eventConfig, eventHandlers = {}) {
|
|
15
|
+
this.name = name;
|
|
16
|
+
this.question = question;
|
|
17
|
+
this.type = type;
|
|
18
|
+
this.eventConfig = eventConfig;
|
|
19
|
+
this.eventHandlers = eventHandlers;
|
|
20
|
+
}
|
|
21
|
+
addBehavior(event, behavior) {
|
|
22
|
+
if (!this.eventConfig) {
|
|
23
|
+
this.eventConfig = {};
|
|
24
|
+
}
|
|
25
|
+
if (!this.eventConfig[event]) {
|
|
26
|
+
this.eventConfig[event] = [];
|
|
27
|
+
}
|
|
28
|
+
if (behavior.type === FieldBehaviorType.MOVE_TO) {
|
|
29
|
+
const existingMoveToIndex = this.eventConfig[event].findIndex(b => b.type === FieldBehaviorType.MOVE_TO);
|
|
30
|
+
if (existingMoveToIndex !== -1) {
|
|
31
|
+
this.eventConfig[event][existingMoveToIndex] = behavior;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
this.eventConfig[event].push(behavior);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.eventConfig[event].push(behavior);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
onFocus(callback) {
|
|
42
|
+
this.eventHandlers.onFocus = callback;
|
|
43
|
+
}
|
|
44
|
+
onChange(callback) {
|
|
45
|
+
this.eventHandlers.onChange = callback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var FieldEvents;
|
|
49
|
+
(function (FieldEvents) {
|
|
50
|
+
FieldEvents["VALID_ANSWER"] = "validAnswer";
|
|
51
|
+
FieldEvents["INVALID_ANSWER"] = "invalidAnswer";
|
|
52
|
+
FieldEvents["MOVE_REQUESTED"] = "moveRequested";
|
|
53
|
+
FieldEvents["END_REQUESTED"] = "endRequested";
|
|
54
|
+
FieldEvents["VALID_YES_ANSWER"] = "validYesAnswer";
|
|
55
|
+
FieldEvents["VALID_NO_ANSWER"] = "validNoAnswer";
|
|
56
|
+
})(FieldEvents || (FieldEvents = {}));
|
|
57
|
+
var FieldBehaviorType;
|
|
58
|
+
(function (FieldBehaviorType) {
|
|
59
|
+
FieldBehaviorType["MOVE_TO"] = "moveTo";
|
|
60
|
+
FieldBehaviorType["OUTPUT"] = "output";
|
|
61
|
+
})(FieldBehaviorType || (FieldBehaviorType = {}));
|
|
62
|
+
export class TextField extends Field {
|
|
63
|
+
constructor(name, question, eventConfig) {
|
|
64
|
+
super(name, question, FieldType.TEXT, eventConfig);
|
|
65
|
+
}
|
|
66
|
+
addValidation(validation) {
|
|
67
|
+
this.textFieldValidation = validation;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class NumberField extends Field {
|
|
71
|
+
constructor(name, question, eventConfig) {
|
|
72
|
+
super(name, question, FieldType.NUMBER, eventConfig);
|
|
73
|
+
}
|
|
74
|
+
addValidation(validation) {
|
|
75
|
+
this.numberFieldValidation = validation;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class SelectField extends Field {
|
|
79
|
+
constructor(name, question, eventConfig) {
|
|
80
|
+
super(name, question, FieldType.SELECT, eventConfig);
|
|
81
|
+
this.selectFieldValidation = {
|
|
82
|
+
validate: true,
|
|
83
|
+
selectOptions: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
addSelectOption(option) {
|
|
87
|
+
this.selectFieldValidation?.selectOptions?.push(option);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class MultiselectField extends Field {
|
|
91
|
+
constructor(name, question, eventConfig) {
|
|
92
|
+
super(name, question, FieldType.MULTISELECT, eventConfig);
|
|
93
|
+
}
|
|
94
|
+
addSelectOption(option) {
|
|
95
|
+
this.multiselectFieldValidation?.selectOptions?.push(option);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class YesNoField extends Field {
|
|
99
|
+
constructor(name, question, eventConfig) {
|
|
100
|
+
super(name, question, FieldType.YESNO, eventConfig);
|
|
101
|
+
}
|
|
102
|
+
addValidation(validation) {
|
|
103
|
+
this.yesNoFieldValidation = validation;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export class TextAreaField extends Field {
|
|
107
|
+
constructor(name, question, eventConfig) {
|
|
108
|
+
super(name, question, FieldType.TEXTAREA, eventConfig);
|
|
109
|
+
}
|
|
110
|
+
addValidation(validation) {
|
|
111
|
+
this.textAreaFieldValidation = validation;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export class InfoField extends Field {
|
|
115
|
+
constructor(name, question, eventConfig) {
|
|
116
|
+
super(name, question, FieldType.INFO, eventConfig);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class VeformBuilder {
|
|
120
|
+
constructor() {
|
|
121
|
+
this.fields = [];
|
|
122
|
+
}
|
|
123
|
+
addField({ name, question, type }) {
|
|
124
|
+
if (this.getField(name)) {
|
|
125
|
+
console.error(`Field with name ${name} already exists`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (name.length === 0 || question.length === 0) {
|
|
129
|
+
console.error(`Field with name ${name} and question ${question} has invalid name or question`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
let field;
|
|
133
|
+
switch (type) {
|
|
134
|
+
case FieldType.TEXT:
|
|
135
|
+
field = new TextField(name, question);
|
|
136
|
+
break;
|
|
137
|
+
case FieldType.TEXTAREA:
|
|
138
|
+
field = new TextAreaField(name, question);
|
|
139
|
+
break;
|
|
140
|
+
case FieldType.SELECT:
|
|
141
|
+
field = new SelectField(name, question);
|
|
142
|
+
break;
|
|
143
|
+
case FieldType.YESNO:
|
|
144
|
+
field = new YesNoField(name, question);
|
|
145
|
+
break;
|
|
146
|
+
case FieldType.NUMBER:
|
|
147
|
+
field = new NumberField(name, question);
|
|
148
|
+
break;
|
|
149
|
+
case FieldType.INFO:
|
|
150
|
+
field = new InfoField(name, question);
|
|
151
|
+
break;
|
|
152
|
+
case FieldType.MULTISELECT:
|
|
153
|
+
field = new MultiselectField(name, question);
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
console.error(`Field with name ${name} has invalid type ${type}`);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
this.fields.push(field);
|
|
160
|
+
return field;
|
|
161
|
+
}
|
|
162
|
+
getField(name) {
|
|
163
|
+
return this.fields.find(field => field.name === name);
|
|
164
|
+
}
|
|
165
|
+
getFields() {
|
|
166
|
+
return this.fields;
|
|
167
|
+
}
|
|
168
|
+
setField(name, field) {
|
|
169
|
+
const index = this.fields.findIndex(field => field.name === name);
|
|
170
|
+
if (index !== -1) {
|
|
171
|
+
this.fields[index] = field;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
removeField(name) {
|
|
177
|
+
const index = this.fields.findIndex(field => field.name === name);
|
|
178
|
+
if (index !== -1) {
|
|
179
|
+
this.fields.splice(index, 1);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var Veform = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// dist/index.js
|
|
22
|
+
var dist_exports = {};
|
|
23
|
+
__export(dist_exports, {
|
|
24
|
+
Field: () => Field,
|
|
25
|
+
InfoField: () => InfoField,
|
|
26
|
+
MultiselectField: () => MultiselectField,
|
|
27
|
+
NumberField: () => NumberField,
|
|
28
|
+
SelectField: () => SelectField,
|
|
29
|
+
TextAreaField: () => TextAreaField,
|
|
30
|
+
TextField: () => TextField,
|
|
31
|
+
Veform: () => Veform,
|
|
32
|
+
VeformBuilder: () => VeformBuilder,
|
|
33
|
+
YesNoField: () => YesNoField
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// dist/veform-builder.js
|
|
37
|
+
var FieldType;
|
|
38
|
+
(function(FieldType2) {
|
|
39
|
+
FieldType2["TEXT"] = "text";
|
|
40
|
+
FieldType2["TEXTAREA"] = "textarea";
|
|
41
|
+
FieldType2["SELECT"] = "select";
|
|
42
|
+
FieldType2["MULTISELECT"] = "multiselect";
|
|
43
|
+
FieldType2["YESNO"] = "yesNo";
|
|
44
|
+
FieldType2["NUMBER"] = "number";
|
|
45
|
+
FieldType2["INFO"] = "info";
|
|
46
|
+
})(FieldType || (FieldType = {}));
|
|
47
|
+
var Field = class {
|
|
48
|
+
constructor(name, question, type, eventConfig, eventHandlers = {}) {
|
|
49
|
+
this.name = name;
|
|
50
|
+
this.question = question;
|
|
51
|
+
this.type = type;
|
|
52
|
+
this.eventConfig = eventConfig;
|
|
53
|
+
this.eventHandlers = eventHandlers;
|
|
54
|
+
}
|
|
55
|
+
addBehavior(event, behavior) {
|
|
56
|
+
if (!this.eventConfig) {
|
|
57
|
+
this.eventConfig = {};
|
|
58
|
+
}
|
|
59
|
+
if (!this.eventConfig[event]) {
|
|
60
|
+
this.eventConfig[event] = [];
|
|
61
|
+
}
|
|
62
|
+
if (behavior.type === FieldBehaviorType.MOVE_TO) {
|
|
63
|
+
const existingMoveToIndex = this.eventConfig[event].findIndex((b) => b.type === FieldBehaviorType.MOVE_TO);
|
|
64
|
+
if (existingMoveToIndex !== -1) {
|
|
65
|
+
this.eventConfig[event][existingMoveToIndex] = behavior;
|
|
66
|
+
} else {
|
|
67
|
+
this.eventConfig[event].push(behavior);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
this.eventConfig[event].push(behavior);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
onFocus(callback) {
|
|
74
|
+
this.eventHandlers.onFocus = callback;
|
|
75
|
+
}
|
|
76
|
+
onChange(callback) {
|
|
77
|
+
this.eventHandlers.onChange = callback;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var FieldEvents;
|
|
81
|
+
(function(FieldEvents2) {
|
|
82
|
+
FieldEvents2["VALID_ANSWER"] = "validAnswer";
|
|
83
|
+
FieldEvents2["INVALID_ANSWER"] = "invalidAnswer";
|
|
84
|
+
FieldEvents2["MOVE_REQUESTED"] = "moveRequested";
|
|
85
|
+
FieldEvents2["END_REQUESTED"] = "endRequested";
|
|
86
|
+
FieldEvents2["VALID_YES_ANSWER"] = "validYesAnswer";
|
|
87
|
+
FieldEvents2["VALID_NO_ANSWER"] = "validNoAnswer";
|
|
88
|
+
})(FieldEvents || (FieldEvents = {}));
|
|
89
|
+
var FieldBehaviorType;
|
|
90
|
+
(function(FieldBehaviorType2) {
|
|
91
|
+
FieldBehaviorType2["MOVE_TO"] = "moveTo";
|
|
92
|
+
FieldBehaviorType2["OUTPUT"] = "output";
|
|
93
|
+
})(FieldBehaviorType || (FieldBehaviorType = {}));
|
|
94
|
+
var TextField = class extends Field {
|
|
95
|
+
constructor(name, question, eventConfig) {
|
|
96
|
+
super(name, question, FieldType.TEXT, eventConfig);
|
|
97
|
+
}
|
|
98
|
+
addValidation(validation) {
|
|
99
|
+
this.textFieldValidation = validation;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var NumberField = class extends Field {
|
|
103
|
+
constructor(name, question, eventConfig) {
|
|
104
|
+
super(name, question, FieldType.NUMBER, eventConfig);
|
|
105
|
+
}
|
|
106
|
+
addValidation(validation) {
|
|
107
|
+
this.numberFieldValidation = validation;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var SelectField = class extends Field {
|
|
111
|
+
constructor(name, question, eventConfig) {
|
|
112
|
+
super(name, question, FieldType.SELECT, eventConfig);
|
|
113
|
+
this.selectFieldValidation = {
|
|
114
|
+
validate: true,
|
|
115
|
+
selectOptions: []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
addSelectOption(option) {
|
|
119
|
+
this.selectFieldValidation?.selectOptions?.push(option);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var MultiselectField = class extends Field {
|
|
123
|
+
constructor(name, question, eventConfig) {
|
|
124
|
+
super(name, question, FieldType.MULTISELECT, eventConfig);
|
|
125
|
+
}
|
|
126
|
+
addSelectOption(option) {
|
|
127
|
+
this.multiselectFieldValidation?.selectOptions?.push(option);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var YesNoField = class extends Field {
|
|
131
|
+
constructor(name, question, eventConfig) {
|
|
132
|
+
super(name, question, FieldType.YESNO, eventConfig);
|
|
133
|
+
}
|
|
134
|
+
addValidation(validation) {
|
|
135
|
+
this.yesNoFieldValidation = validation;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var TextAreaField = class extends Field {
|
|
139
|
+
constructor(name, question, eventConfig) {
|
|
140
|
+
super(name, question, FieldType.TEXTAREA, eventConfig);
|
|
141
|
+
}
|
|
142
|
+
addValidation(validation) {
|
|
143
|
+
this.textAreaFieldValidation = validation;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var InfoField = class extends Field {
|
|
147
|
+
constructor(name, question, eventConfig) {
|
|
148
|
+
super(name, question, FieldType.INFO, eventConfig);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var VeformBuilder = class {
|
|
152
|
+
constructor() {
|
|
153
|
+
this.fields = [];
|
|
154
|
+
}
|
|
155
|
+
addField({ name, question, type }) {
|
|
156
|
+
if (this.getField(name)) {
|
|
157
|
+
console.error(`Field with name ${name} already exists`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (name.length === 0 || question.length === 0) {
|
|
161
|
+
console.error(`Field with name ${name} and question ${question} has invalid name or question`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
let field;
|
|
165
|
+
switch (type) {
|
|
166
|
+
case FieldType.TEXT:
|
|
167
|
+
field = new TextField(name, question);
|
|
168
|
+
break;
|
|
169
|
+
case FieldType.TEXTAREA:
|
|
170
|
+
field = new TextAreaField(name, question);
|
|
171
|
+
break;
|
|
172
|
+
case FieldType.SELECT:
|
|
173
|
+
field = new SelectField(name, question);
|
|
174
|
+
break;
|
|
175
|
+
case FieldType.YESNO:
|
|
176
|
+
field = new YesNoField(name, question);
|
|
177
|
+
break;
|
|
178
|
+
case FieldType.NUMBER:
|
|
179
|
+
field = new NumberField(name, question);
|
|
180
|
+
break;
|
|
181
|
+
case FieldType.INFO:
|
|
182
|
+
field = new InfoField(name, question);
|
|
183
|
+
break;
|
|
184
|
+
case FieldType.MULTISELECT:
|
|
185
|
+
field = new MultiselectField(name, question);
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
console.error(`Field with name ${name} has invalid type ${type}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
this.fields.push(field);
|
|
192
|
+
return field;
|
|
193
|
+
}
|
|
194
|
+
getField(name) {
|
|
195
|
+
return this.fields.find((field) => field.name === name);
|
|
196
|
+
}
|
|
197
|
+
getFields() {
|
|
198
|
+
return this.fields;
|
|
199
|
+
}
|
|
200
|
+
setField(name, field) {
|
|
201
|
+
const index = this.fields.findIndex((field2) => field2.name === name);
|
|
202
|
+
if (index !== -1) {
|
|
203
|
+
this.fields[index] = field;
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
removeField(name) {
|
|
209
|
+
const index = this.fields.findIndex((field) => field.name === name);
|
|
210
|
+
if (index !== -1) {
|
|
211
|
+
this.fields.splice(index, 1);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// dist/veform.js
|
|
219
|
+
var DEFAULT_SERVER_URL = "ws://localhost:8080/ws";
|
|
220
|
+
var Veform = class {
|
|
221
|
+
constructor(fields) {
|
|
222
|
+
this.connected = false;
|
|
223
|
+
this.eventHandlers = {};
|
|
224
|
+
this.localStream = null;
|
|
225
|
+
this.peerConnection = null;
|
|
226
|
+
this.wsConnection = null;
|
|
227
|
+
this.audioElement = null;
|
|
228
|
+
if (fields instanceof VeformBuilder) {
|
|
229
|
+
this.form = { fields: fields.getFields() };
|
|
230
|
+
} else {
|
|
231
|
+
this.form = { fields };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
onLoadingStarted(callback) {
|
|
235
|
+
this.eventHandlers.onLoadingStarted = callback;
|
|
236
|
+
}
|
|
237
|
+
onRunningStarted(callback) {
|
|
238
|
+
this.eventHandlers.onRunningStarted = callback;
|
|
239
|
+
}
|
|
240
|
+
onFinished(callback) {
|
|
241
|
+
this.eventHandlers.onFinished = callback;
|
|
242
|
+
}
|
|
243
|
+
onError(callback) {
|
|
244
|
+
this.eventHandlers.onError = callback;
|
|
245
|
+
}
|
|
246
|
+
onCriticalError(callback) {
|
|
247
|
+
this.eventHandlers.onCriticalError = callback;
|
|
248
|
+
}
|
|
249
|
+
onAudioInStart(callback) {
|
|
250
|
+
this.eventHandlers.onAudioInStart = callback;
|
|
251
|
+
}
|
|
252
|
+
onAudioInEnd(callback) {
|
|
253
|
+
this.eventHandlers.onAudioInEnd = callback;
|
|
254
|
+
}
|
|
255
|
+
onAudioOutStart(callback) {
|
|
256
|
+
this.eventHandlers.onAudioOutStart = callback;
|
|
257
|
+
}
|
|
258
|
+
onAudioOutEnd(callback) {
|
|
259
|
+
this.eventHandlers.onAudioOutEnd = callback;
|
|
260
|
+
}
|
|
261
|
+
onFieldValueChanged(callback) {
|
|
262
|
+
this.eventHandlers.onFieldValueChanged = callback;
|
|
263
|
+
}
|
|
264
|
+
onFocusChanged(callback) {
|
|
265
|
+
this.eventHandlers.onFocusChanged = callback;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Start the conversation
|
|
269
|
+
* This will connect the client the the veform server with the current set of fields
|
|
270
|
+
*/
|
|
271
|
+
async start(token) {
|
|
272
|
+
if (!this.form?.fields || this.form?.fields.length === 0) {
|
|
273
|
+
console.error("No fields provided or token URL");
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (this.connected || this.wsConnection || this.peerConnection || this.localStream) {
|
|
277
|
+
console.error("Start already called, try running stop() or creating a new instance");
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
if (token.startsWith("http")) {
|
|
282
|
+
console.log("Fetching token from URL", token);
|
|
283
|
+
token = await fetch(token, { method: "POST" }).then((response) => response.json()).then((data) => data.token);
|
|
284
|
+
}
|
|
285
|
+
if (!token) {
|
|
286
|
+
console.error("No token provided or returned from token URL");
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
this.audioElement = createAudioElement();
|
|
290
|
+
if (this.eventHandlers.onLoadingStarted) {
|
|
291
|
+
this.eventHandlers.onLoadingStarted();
|
|
292
|
+
}
|
|
293
|
+
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
294
|
+
audio: {
|
|
295
|
+
echoCancellation: true,
|
|
296
|
+
noiseSuppression: true,
|
|
297
|
+
sampleRate: 48e3,
|
|
298
|
+
channelCount: 1
|
|
299
|
+
},
|
|
300
|
+
video: false
|
|
301
|
+
});
|
|
302
|
+
this.peerConnection = new RTCPeerConnection();
|
|
303
|
+
this.localStream.getTracks().forEach((track) => {
|
|
304
|
+
if (!this.localStream || !this.peerConnection) {
|
|
305
|
+
console.error("Local stream or peer connection not established");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.peerConnection?.addTrack(track, this.localStream);
|
|
309
|
+
});
|
|
310
|
+
this.peerConnection.oniceconnectionstatechange = () => {
|
|
311
|
+
if (this.peerConnection?.iceConnectionState === "connected") {
|
|
312
|
+
this.connected = true;
|
|
313
|
+
console.log("Peer connection connected");
|
|
314
|
+
} else if (this.peerConnection?.iceConnectionState === "disconnected") {
|
|
315
|
+
console.log("Peer connection disconnected");
|
|
316
|
+
} else if (this.peerConnection?.iceConnectionState === "failed") {
|
|
317
|
+
if (this.eventHandlers.onError) {
|
|
318
|
+
this.eventHandlers.onError("Connection to server failed");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
this.peerConnection.ontrack = (event) => {
|
|
323
|
+
if (!this.audioElement) {
|
|
324
|
+
console.error("Audio element not found");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const stream = event.streams[0];
|
|
328
|
+
this.audioElement.srcObject = stream;
|
|
329
|
+
this.audioElement.play().catch((e) => console.error("Play error:", e));
|
|
330
|
+
};
|
|
331
|
+
this.wsConnection = new WebSocket(DEFAULT_SERVER_URL + "?token=" + token);
|
|
332
|
+
this.wsConnection.onmessage = (event) => {
|
|
333
|
+
const message = JSON.parse(event.data);
|
|
334
|
+
if (!this.peerConnection) {
|
|
335
|
+
console.error("WS response, Peer connection not established");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (message.type === "answer") {
|
|
339
|
+
const answer = new RTCSessionDescription(message.payload);
|
|
340
|
+
this.peerConnection.setRemoteDescription(answer);
|
|
341
|
+
} else if (message.type === "ice-candidate") {
|
|
342
|
+
const candidate = new RTCIceCandidate(message.payload);
|
|
343
|
+
this.peerConnection.addIceCandidate(candidate);
|
|
344
|
+
} else {
|
|
345
|
+
this.resolveWsMessage(message);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
this.wsConnection.onopen = async () => {
|
|
349
|
+
if (!this.peerConnection) {
|
|
350
|
+
console.error("Peer connection not established");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const offer = await this.peerConnection.createOffer();
|
|
354
|
+
await this.peerConnection.setLocalDescription(offer);
|
|
355
|
+
this.peerConnection.onicecandidate = (event) => {
|
|
356
|
+
if (event.candidate) {
|
|
357
|
+
this.wsConnection?.send(JSON.stringify({
|
|
358
|
+
type: "ice-candidate",
|
|
359
|
+
payload: event.candidate
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
this.wsConnection?.send(JSON.stringify({
|
|
364
|
+
type: "offer",
|
|
365
|
+
payload: this.peerConnection?.localDescription
|
|
366
|
+
}));
|
|
367
|
+
this.wsConnection?.send(JSON.stringify({
|
|
368
|
+
type: "form",
|
|
369
|
+
payload: this.form
|
|
370
|
+
}));
|
|
371
|
+
};
|
|
372
|
+
return true;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error("Error starting conversation:", error);
|
|
375
|
+
this.connected = false;
|
|
376
|
+
this.wsConnection = null;
|
|
377
|
+
this.peerConnection = null;
|
|
378
|
+
this.localStream = null;
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Stop the conversation, this will cut off audio mid output
|
|
384
|
+
*/
|
|
385
|
+
stop() {
|
|
386
|
+
console.log("Stop called");
|
|
387
|
+
if (!this.connected) {
|
|
388
|
+
console.error("Not connected to veform server");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.connected = false;
|
|
392
|
+
this.wsConnection?.close();
|
|
393
|
+
this.wsConnection = null;
|
|
394
|
+
this.peerConnection?.close();
|
|
395
|
+
this.peerConnection = null;
|
|
396
|
+
this.localStream?.getTracks().forEach((track) => {
|
|
397
|
+
track.stop();
|
|
398
|
+
});
|
|
399
|
+
this.localStream = null;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Emit audio to client
|
|
403
|
+
* interrupt: if true, will interrupt the current output, otherwise will be queued behind current audio
|
|
404
|
+
*/
|
|
405
|
+
emitAudio(audio, interrupt = false) {
|
|
406
|
+
console.log("Emit audio called", audio);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Change the current field
|
|
410
|
+
* This will change the current field to the one provided
|
|
411
|
+
*/
|
|
412
|
+
changeField(fieldName, interrupt = false) {
|
|
413
|
+
console.log("Change field called", fieldName, interrupt);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Stop current audio output
|
|
417
|
+
*/
|
|
418
|
+
interrupt() {
|
|
419
|
+
console.log("Interrupt called");
|
|
420
|
+
}
|
|
421
|
+
resolveWsMessage(message) {
|
|
422
|
+
console.log("RAW WS MESSAGE:", message);
|
|
423
|
+
switch (message.type) {
|
|
424
|
+
case "event-start":
|
|
425
|
+
if (this.eventHandlers.onRunningStarted) {
|
|
426
|
+
this.eventHandlers.onRunningStarted();
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
case "event-end":
|
|
430
|
+
if (this.eventHandlers.onFinished) {
|
|
431
|
+
this.eventHandlers.onFinished();
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
case "event-audio-out-start":
|
|
435
|
+
if (this.eventHandlers.onAudioOutStart) {
|
|
436
|
+
const interrupt = this.eventHandlers.onAudioOutStart(message.payload);
|
|
437
|
+
if (interrupt) {
|
|
438
|
+
console.log("Audio out start interrupted");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
case "event-audio-out-end":
|
|
443
|
+
if (this.eventHandlers.onAudioOutEnd) {
|
|
444
|
+
this.eventHandlers.onAudioOutEnd();
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
case "event-input-start":
|
|
448
|
+
if (this.eventHandlers.onAudioInStart) {
|
|
449
|
+
this.eventHandlers.onAudioInStart();
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
case "event-input-end":
|
|
453
|
+
if (this.eventHandlers.onAudioInEnd) {
|
|
454
|
+
const interrupt = this.eventHandlers.onAudioInEnd(message.payload);
|
|
455
|
+
if (interrupt) {
|
|
456
|
+
console.log("Audio in end interrupted");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
case "event-focus-changed":
|
|
461
|
+
if (this.eventHandlers.onFocusChanged) {
|
|
462
|
+
const interrupt = this.eventHandlers.onFocusChanged(message.payload.previousName, message.payload.nextName);
|
|
463
|
+
if (interrupt) {
|
|
464
|
+
console.log("Focus changed interrupted");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const nextField = this.form.fields.find((field2) => field2.name === message.payload.nextName);
|
|
469
|
+
if (nextField?.eventHandlers?.onFocus) {
|
|
470
|
+
nextField.eventHandlers.onFocus(message.payload.previousName);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
case "event-field-value-changed":
|
|
474
|
+
console.log("CHECKING FOR FIELD:", message.payload);
|
|
475
|
+
const field = this.form.fields.find((field2) => field2.name === message.payload.fieldName);
|
|
476
|
+
console.log("FIELD:", field);
|
|
477
|
+
if (field?.eventHandlers?.onChange) {
|
|
478
|
+
field.eventHandlers.onChange(message.payload.value);
|
|
479
|
+
}
|
|
480
|
+
if (this.eventHandlers.onFieldValueChanged) {
|
|
481
|
+
this.eventHandlers.onFieldValueChanged(message.payload.fieldName, message.payload.value);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
case "event-error":
|
|
485
|
+
if (this.eventHandlers.onError) {
|
|
486
|
+
this.eventHandlers.onError(message.payload);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
case "event-critical-error":
|
|
490
|
+
if (this.eventHandlers.onCriticalError) {
|
|
491
|
+
this.eventHandlers.onCriticalError(message.payload);
|
|
492
|
+
this.stop();
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
default:
|
|
496
|
+
console.error("Unknown event type:", message.type);
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function createAudioElement() {
|
|
502
|
+
const element = document.createElement("audio");
|
|
503
|
+
element.id = "veform-audio";
|
|
504
|
+
element.style.opacity = "0";
|
|
505
|
+
element.style.position = "absolute";
|
|
506
|
+
element.style.bottom = "0";
|
|
507
|
+
element.style.right = "0";
|
|
508
|
+
element.style.width = "100%";
|
|
509
|
+
element.style.height = "100%";
|
|
510
|
+
document.body.appendChild(element);
|
|
511
|
+
return element;
|
|
512
|
+
}
|
|
513
|
+
return __toCommonJS(dist_exports);
|
|
514
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var Veform=(()=>{var C=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var w=(t,e)=>{for(var n in e)C(t,n,{get:e[n],enumerable:!0})},A=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of H(e))!m.call(t,i)&&i!==n&&C(t,i,{get:()=>e[i],enumerable:!(o=y(e,i))||o.enumerable});return t};var O=t=>A(C({},"__esModule",{value:!0}),t);var R={};w(R,{Field:()=>s,InfoField:()=>v,MultiselectField:()=>u,NumberField:()=>c,SelectField:()=>h,TextAreaField:()=>p,TextField:()=>d,Veform:()=>E,VeformBuilder:()=>a,YesNoField:()=>f});var r;(function(t){t.TEXT="text",t.TEXTAREA="textarea",t.SELECT="select",t.MULTISELECT="multiselect",t.YESNO="yesNo",t.NUMBER="number",t.INFO="info"})(r||(r={}));var s=class{constructor(e,n,o,i,g={}){this.name=e,this.question=n,this.type=o,this.eventConfig=i,this.eventHandlers=g}addBehavior(e,n){if(this.eventConfig||(this.eventConfig={}),this.eventConfig[e]||(this.eventConfig[e]=[]),n.type===l.MOVE_TO){let o=this.eventConfig[e].findIndex(i=>i.type===l.MOVE_TO);o!==-1?this.eventConfig[e][o]=n:this.eventConfig[e].push(n)}else this.eventConfig[e].push(n)}onFocus(e){this.eventHandlers.onFocus=e}onChange(e){this.eventHandlers.onChange=e}},S;(function(t){t.VALID_ANSWER="validAnswer",t.INVALID_ANSWER="invalidAnswer",t.MOVE_REQUESTED="moveRequested",t.END_REQUESTED="endRequested",t.VALID_YES_ANSWER="validYesAnswer",t.VALID_NO_ANSWER="validNoAnswer"})(S||(S={}));var l;(function(t){t.MOVE_TO="moveTo",t.OUTPUT="output"})(l||(l={}));var d=class extends s{constructor(e,n,o){super(e,n,r.TEXT,o)}addValidation(e){this.textFieldValidation=e}},c=class extends s{constructor(e,n,o){super(e,n,r.NUMBER,o)}addValidation(e){this.numberFieldValidation=e}},h=class extends s{constructor(e,n,o){super(e,n,r.SELECT,o),this.selectFieldValidation={validate:!0,selectOptions:[]}}addSelectOption(e){this.selectFieldValidation?.selectOptions?.push(e)}},u=class extends s{constructor(e,n,o){super(e,n,r.MULTISELECT,o)}addSelectOption(e){this.multiselectFieldValidation?.selectOptions?.push(e)}},f=class extends s{constructor(e,n,o){super(e,n,r.YESNO,o)}addValidation(e){this.yesNoFieldValidation=e}},p=class extends s{constructor(e,n,o){super(e,n,r.TEXTAREA,o)}addValidation(e){this.textAreaFieldValidation=e}},v=class extends s{constructor(e,n,o){super(e,n,r.INFO,o)}},a=class{constructor(){this.fields=[]}addField({name:e,question:n,type:o}){if(this.getField(e))return console.error(`Field with name ${e} already exists`),null;if(e.length===0||n.length===0)return console.error(`Field with name ${e} and question ${n} has invalid name or question`),null;let i;switch(o){case r.TEXT:i=new d(e,n);break;case r.TEXTAREA:i=new p(e,n);break;case r.SELECT:i=new h(e,n);break;case r.YESNO:i=new f(e,n);break;case r.NUMBER:i=new c(e,n);break;case r.INFO:i=new v(e,n);break;case r.MULTISELECT:i=new u(e,n);break;default:return console.error(`Field with name ${e} has invalid type ${o}`),null}return this.fields.push(i),i}getField(e){return this.fields.find(n=>n.name===e)}getFields(){return this.fields}setField(e,n){let o=this.fields.findIndex(i=>i.name===e);return o!==-1?(this.fields[o]=n,!0):!1}removeField(e){let n=this.fields.findIndex(o=>o.name===e);return n!==-1?(this.fields.splice(n,1),!0):!1}};var x="ws://localhost:8080/ws",E=class{constructor(e){this.connected=!1,this.eventHandlers={},this.localStream=null,this.peerConnection=null,this.wsConnection=null,this.audioElement=null,e instanceof a?this.form={fields:e.getFields()}:this.form={fields:e}}onLoadingStarted(e){this.eventHandlers.onLoadingStarted=e}onRunningStarted(e){this.eventHandlers.onRunningStarted=e}onFinished(e){this.eventHandlers.onFinished=e}onError(e){this.eventHandlers.onError=e}onCriticalError(e){this.eventHandlers.onCriticalError=e}onAudioInStart(e){this.eventHandlers.onAudioInStart=e}onAudioInEnd(e){this.eventHandlers.onAudioInEnd=e}onAudioOutStart(e){this.eventHandlers.onAudioOutStart=e}onAudioOutEnd(e){this.eventHandlers.onAudioOutEnd=e}onFieldValueChanged(e){this.eventHandlers.onFieldValueChanged=e}onFocusChanged(e){this.eventHandlers.onFocusChanged=e}async start(e){if(!this.form?.fields||this.form?.fields.length===0)return console.error("No fields provided or token URL"),!1;if(this.connected||this.wsConnection||this.peerConnection||this.localStream)return console.error("Start already called, try running stop() or creating a new instance"),!1;try{return e.startsWith("http")&&(console.log("Fetching token from URL",e),e=await fetch(e,{method:"POST"}).then(n=>n.json()).then(n=>n.token)),e?(this.audioElement=N(),this.eventHandlers.onLoadingStarted&&this.eventHandlers.onLoadingStarted(),this.localStream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,sampleRate:48e3,channelCount:1},video:!1}),this.peerConnection=new RTCPeerConnection,this.localStream.getTracks().forEach(n=>{if(!this.localStream||!this.peerConnection){console.error("Local stream or peer connection not established");return}this.peerConnection?.addTrack(n,this.localStream)}),this.peerConnection.oniceconnectionstatechange=()=>{this.peerConnection?.iceConnectionState==="connected"?(this.connected=!0,console.log("Peer connection connected")):this.peerConnection?.iceConnectionState==="disconnected"?console.log("Peer connection disconnected"):this.peerConnection?.iceConnectionState==="failed"&&this.eventHandlers.onError&&this.eventHandlers.onError("Connection to server failed")},this.peerConnection.ontrack=n=>{if(!this.audioElement){console.error("Audio element not found");return}let o=n.streams[0];this.audioElement.srcObject=o,this.audioElement.play().catch(i=>console.error("Play error:",i))},this.wsConnection=new WebSocket(x+"?token="+e),this.wsConnection.onmessage=n=>{let o=JSON.parse(n.data);if(!this.peerConnection){console.error("WS response, Peer connection not established");return}if(o.type==="answer"){let i=new RTCSessionDescription(o.payload);this.peerConnection.setRemoteDescription(i)}else if(o.type==="ice-candidate"){let i=new RTCIceCandidate(o.payload);this.peerConnection.addIceCandidate(i)}else this.resolveWsMessage(o)},this.wsConnection.onopen=async()=>{if(!this.peerConnection){console.error("Peer connection not established");return}let n=await this.peerConnection.createOffer();await this.peerConnection.setLocalDescription(n),this.peerConnection.onicecandidate=o=>{o.candidate&&this.wsConnection?.send(JSON.stringify({type:"ice-candidate",payload:o.candidate}))},this.wsConnection?.send(JSON.stringify({type:"offer",payload:this.peerConnection?.localDescription})),this.wsConnection?.send(JSON.stringify({type:"form",payload:this.form}))},!0):(console.error("No token provided or returned from token URL"),!1)}catch(n){return console.error("Error starting conversation:",n),this.connected=!1,this.wsConnection=null,this.peerConnection=null,this.localStream=null,!1}}stop(){if(console.log("Stop called"),!this.connected){console.error("Not connected to veform server");return}this.connected=!1,this.wsConnection?.close(),this.wsConnection=null,this.peerConnection?.close(),this.peerConnection=null,this.localStream?.getTracks().forEach(e=>{e.stop()}),this.localStream=null}emitAudio(e,n=!1){console.log("Emit audio called",e)}changeField(e,n=!1){console.log("Change field called",e,n)}interrupt(){console.log("Interrupt called")}resolveWsMessage(e){switch(console.log("RAW WS MESSAGE:",e),e.type){case"event-start":this.eventHandlers.onRunningStarted&&this.eventHandlers.onRunningStarted();return;case"event-end":this.eventHandlers.onFinished&&this.eventHandlers.onFinished();return;case"event-audio-out-start":this.eventHandlers.onAudioOutStart&&this.eventHandlers.onAudioOutStart(e.payload)&&console.log("Audio out start interrupted");return;case"event-audio-out-end":this.eventHandlers.onAudioOutEnd&&this.eventHandlers.onAudioOutEnd();return;case"event-input-start":this.eventHandlers.onAudioInStart&&this.eventHandlers.onAudioInStart();return;case"event-input-end":this.eventHandlers.onAudioInEnd&&this.eventHandlers.onAudioInEnd(e.payload)&&console.log("Audio in end interrupted");return;case"event-focus-changed":if(this.eventHandlers.onFocusChanged&&this.eventHandlers.onFocusChanged(e.payload.previousName,e.payload.nextName)){console.log("Focus changed interrupted");return}let n=this.form.fields.find(i=>i.name===e.payload.nextName);n?.eventHandlers?.onFocus&&n.eventHandlers.onFocus(e.payload.previousName);return;case"event-field-value-changed":console.log("CHECKING FOR FIELD:",e.payload);let o=this.form.fields.find(i=>i.name===e.payload.fieldName);console.log("FIELD:",o),o?.eventHandlers?.onChange&&o.eventHandlers.onChange(e.payload.value),this.eventHandlers.onFieldValueChanged&&this.eventHandlers.onFieldValueChanged(e.payload.fieldName,e.payload.value);return;case"event-error":this.eventHandlers.onError&&this.eventHandlers.onError(e.payload);return;case"event-critical-error":this.eventHandlers.onCriticalError&&(this.eventHandlers.onCriticalError(e.payload),this.stop());return;default:console.error("Unknown event type:",e.type);break}}};function N(){let t=document.createElement("audio");return t.id="veform-audio",t.style.opacity="0",t.style.position="absolute",t.style.bottom="0",t.style.right="0",t.style.width="100%",t.style.height="100%",document.body.appendChild(t),t}return O(R);})();
|
package/dist/veform.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Field, VeformBuilder } from './veform-builder';
|
|
2
|
+
type EventHandlers = {
|
|
3
|
+
/**
|
|
4
|
+
* Called immediately after start() is called
|
|
5
|
+
*/
|
|
6
|
+
onLoadingStarted?: () => void;
|
|
7
|
+
/**
|
|
8
|
+
* Called when connections established and conversation begins
|
|
9
|
+
* This can be treated as a loading finished event
|
|
10
|
+
*/
|
|
11
|
+
onRunningStarted?: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Called when conversation is complete
|
|
14
|
+
*/
|
|
15
|
+
onFinished?: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Called when an error occurs.
|
|
18
|
+
* Veform will attempt to recover from these errors.
|
|
19
|
+
*/
|
|
20
|
+
onError?: (error: string) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Called when a critical error occurs.
|
|
23
|
+
* veform audio will output a generic error and end the conversation when these occur.
|
|
24
|
+
*/
|
|
25
|
+
onCriticalError?: (error: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Called when user talking is detected
|
|
28
|
+
*/
|
|
29
|
+
onAudioInStart?: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* Called when user done talking is detected
|
|
32
|
+
Returning true blocks veform from processing this input and continuing the conversation.
|
|
33
|
+
* If you block this you must call `changeField` or`emitAudio` to keep the conversation going.
|
|
34
|
+
*/
|
|
35
|
+
onAudioInEnd?: (input: string) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Called before audio output starts.
|
|
38
|
+
* Returning true blocks veform from emitting this output and continuing the conversation.
|
|
39
|
+
* If you block this you must call `changeField` or`emitAudio` to keep the conversation going.
|
|
40
|
+
*/
|
|
41
|
+
onAudioOutStart?: (chunk: string) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Called when audio output ends
|
|
44
|
+
*/
|
|
45
|
+
onAudioOutEnd?: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Called when the current field focus changes
|
|
48
|
+
* Returning true blocks veform from changing the field and continuing the conversation.
|
|
49
|
+
* If you block this you must call `changeField` or `emitAudio` to keep the conversation going.
|
|
50
|
+
*/
|
|
51
|
+
onFocusChanged?: (previousName: string, nextName: string) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Called when an answer is provided to a field
|
|
54
|
+
*/
|
|
55
|
+
onFieldValueChanged?: (fieldName: string, answer: string | number | boolean) => void;
|
|
56
|
+
};
|
|
57
|
+
export declare class Veform {
|
|
58
|
+
private connected;
|
|
59
|
+
private form;
|
|
60
|
+
private eventHandlers;
|
|
61
|
+
private localStream;
|
|
62
|
+
private peerConnection;
|
|
63
|
+
private wsConnection;
|
|
64
|
+
private audioElement;
|
|
65
|
+
constructor(fields: Field[] | VeformBuilder);
|
|
66
|
+
onLoadingStarted(callback: EventHandlers['onLoadingStarted']): void;
|
|
67
|
+
onRunningStarted(callback: EventHandlers['onRunningStarted']): void;
|
|
68
|
+
onFinished(callback: EventHandlers['onFinished']): void;
|
|
69
|
+
onError(callback: EventHandlers['onError']): void;
|
|
70
|
+
onCriticalError(callback: EventHandlers['onCriticalError']): void;
|
|
71
|
+
onAudioInStart(callback: EventHandlers['onAudioInStart']): void;
|
|
72
|
+
onAudioInEnd(callback: EventHandlers['onAudioInEnd']): void;
|
|
73
|
+
onAudioOutStart(callback: EventHandlers['onAudioOutStart']): void;
|
|
74
|
+
onAudioOutEnd(callback: EventHandlers['onAudioOutEnd']): void;
|
|
75
|
+
onFieldValueChanged(callback: EventHandlers['onFieldValueChanged']): void;
|
|
76
|
+
onFocusChanged(callback: EventHandlers['onFocusChanged']): void;
|
|
77
|
+
/**
|
|
78
|
+
* Start the conversation
|
|
79
|
+
* This will connect the client the the veform server with the current set of fields
|
|
80
|
+
*/
|
|
81
|
+
start(token: string): Promise<boolean>;
|
|
82
|
+
/**
|
|
83
|
+
* Stop the conversation, this will cut off audio mid output
|
|
84
|
+
*/
|
|
85
|
+
stop(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Emit audio to client
|
|
88
|
+
* interrupt: if true, will interrupt the current output, otherwise will be queued behind current audio
|
|
89
|
+
*/
|
|
90
|
+
emitAudio(audio: string, interrupt?: boolean): void;
|
|
91
|
+
/**
|
|
92
|
+
* Change the current field
|
|
93
|
+
* This will change the current field to the one provided
|
|
94
|
+
*/
|
|
95
|
+
changeField(fieldName: string, interrupt?: boolean): void;
|
|
96
|
+
/**
|
|
97
|
+
* Stop current audio output
|
|
98
|
+
*/
|
|
99
|
+
interrupt(): void;
|
|
100
|
+
private resolveWsMessage;
|
|
101
|
+
}
|
|
102
|
+
export {};
|
package/dist/veform.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { VeformBuilder } from './veform-builder';
|
|
2
|
+
const DEFAULT_SERVER_URL = 'ws://localhost:8080/ws';
|
|
3
|
+
// this is what exposes all the cancelable events to user
|
|
4
|
+
export class Veform {
|
|
5
|
+
constructor(fields) {
|
|
6
|
+
this.connected = false;
|
|
7
|
+
this.eventHandlers = {};
|
|
8
|
+
this.localStream = null;
|
|
9
|
+
this.peerConnection = null;
|
|
10
|
+
this.wsConnection = null;
|
|
11
|
+
this.audioElement = null;
|
|
12
|
+
if (fields instanceof VeformBuilder) {
|
|
13
|
+
this.form = { fields: fields.getFields() };
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
this.form = { fields: fields };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
onLoadingStarted(callback) {
|
|
20
|
+
this.eventHandlers.onLoadingStarted = callback;
|
|
21
|
+
}
|
|
22
|
+
onRunningStarted(callback) {
|
|
23
|
+
this.eventHandlers.onRunningStarted = callback;
|
|
24
|
+
}
|
|
25
|
+
onFinished(callback) {
|
|
26
|
+
this.eventHandlers.onFinished = callback;
|
|
27
|
+
}
|
|
28
|
+
onError(callback) {
|
|
29
|
+
this.eventHandlers.onError = callback;
|
|
30
|
+
}
|
|
31
|
+
onCriticalError(callback) {
|
|
32
|
+
this.eventHandlers.onCriticalError = callback;
|
|
33
|
+
}
|
|
34
|
+
onAudioInStart(callback) {
|
|
35
|
+
this.eventHandlers.onAudioInStart = callback;
|
|
36
|
+
}
|
|
37
|
+
onAudioInEnd(callback) {
|
|
38
|
+
this.eventHandlers.onAudioInEnd = callback;
|
|
39
|
+
}
|
|
40
|
+
onAudioOutStart(callback) {
|
|
41
|
+
this.eventHandlers.onAudioOutStart = callback;
|
|
42
|
+
}
|
|
43
|
+
onAudioOutEnd(callback) {
|
|
44
|
+
this.eventHandlers.onAudioOutEnd = callback;
|
|
45
|
+
}
|
|
46
|
+
onFieldValueChanged(callback) {
|
|
47
|
+
this.eventHandlers.onFieldValueChanged = callback;
|
|
48
|
+
}
|
|
49
|
+
onFocusChanged(callback) {
|
|
50
|
+
this.eventHandlers.onFocusChanged = callback;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start the conversation
|
|
54
|
+
* This will connect the client the the veform server with the current set of fields
|
|
55
|
+
*/
|
|
56
|
+
async start(token) {
|
|
57
|
+
if (!this.form?.fields || this.form?.fields.length === 0) {
|
|
58
|
+
console.error('No fields provided or token URL');
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (this.connected || this.wsConnection || this.peerConnection || this.localStream) {
|
|
62
|
+
console.error('Start already called, try running stop() or creating a new instance');
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
if (token.startsWith('http')) {
|
|
67
|
+
console.log('Fetching token from URL', token);
|
|
68
|
+
token = await fetch(token, { method: 'POST' }).then(response => response.json()).then(data => data.token);
|
|
69
|
+
}
|
|
70
|
+
if (!token) {
|
|
71
|
+
console.error('No token provided or returned from token URL');
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
this.audioElement = createAudioElement();
|
|
75
|
+
if (this.eventHandlers.onLoadingStarted) {
|
|
76
|
+
this.eventHandlers.onLoadingStarted();
|
|
77
|
+
}
|
|
78
|
+
// get local audio track
|
|
79
|
+
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
80
|
+
audio: {
|
|
81
|
+
echoCancellation: true,
|
|
82
|
+
noiseSuppression: true,
|
|
83
|
+
sampleRate: 48000,
|
|
84
|
+
channelCount: 1,
|
|
85
|
+
},
|
|
86
|
+
video: false,
|
|
87
|
+
});
|
|
88
|
+
// setup local peerconnection
|
|
89
|
+
this.peerConnection = new RTCPeerConnection();
|
|
90
|
+
this.localStream.getTracks().forEach((track) => {
|
|
91
|
+
if (!this.localStream || !this.peerConnection) {
|
|
92
|
+
console.error('Local stream or peer connection not established');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.peerConnection?.addTrack(track, this.localStream);
|
|
96
|
+
});
|
|
97
|
+
this.peerConnection.oniceconnectionstatechange = () => {
|
|
98
|
+
if (this.peerConnection?.iceConnectionState === 'connected') {
|
|
99
|
+
this.connected = true;
|
|
100
|
+
console.log('Peer connection connected');
|
|
101
|
+
}
|
|
102
|
+
else if (this.peerConnection?.iceConnectionState === 'disconnected') {
|
|
103
|
+
console.log('Peer connection disconnected');
|
|
104
|
+
}
|
|
105
|
+
else if (this.peerConnection?.iceConnectionState === 'failed') {
|
|
106
|
+
if (this.eventHandlers.onError) {
|
|
107
|
+
this.eventHandlers.onError('Connection to server failed');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
this.peerConnection.ontrack = (event) => {
|
|
112
|
+
if (!this.audioElement) {
|
|
113
|
+
console.error('Audio element not found');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const stream = event.streams[0];
|
|
117
|
+
this.audioElement.srcObject = stream;
|
|
118
|
+
this.audioElement.play().catch((e) => console.error("Play error:", e));
|
|
119
|
+
};
|
|
120
|
+
// create websocket connection
|
|
121
|
+
this.wsConnection = new WebSocket(DEFAULT_SERVER_URL + '?token=' + token);
|
|
122
|
+
this.wsConnection.onmessage = (event) => {
|
|
123
|
+
const message = JSON.parse(event.data);
|
|
124
|
+
if (!this.peerConnection) {
|
|
125
|
+
console.error('WS response, Peer connection not established');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (message.type === "answer") {
|
|
129
|
+
const answer = new RTCSessionDescription(message.payload);
|
|
130
|
+
this.peerConnection.setRemoteDescription(answer);
|
|
131
|
+
}
|
|
132
|
+
else if (message.type === "ice-candidate") {
|
|
133
|
+
const candidate = new RTCIceCandidate(message.payload);
|
|
134
|
+
this.peerConnection.addIceCandidate(candidate);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this.resolveWsMessage(message);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
this.wsConnection.onopen = async () => {
|
|
141
|
+
if (!this.peerConnection) {
|
|
142
|
+
console.error('Peer connection not established');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const offer = await this.peerConnection.createOffer();
|
|
146
|
+
await this.peerConnection.setLocalDescription(offer);
|
|
147
|
+
this.peerConnection.onicecandidate = (event) => {
|
|
148
|
+
if (event.candidate) {
|
|
149
|
+
this.wsConnection?.send(JSON.stringify({
|
|
150
|
+
type: "ice-candidate",
|
|
151
|
+
payload: event.candidate,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
this.wsConnection?.send(JSON.stringify({
|
|
156
|
+
type: "offer",
|
|
157
|
+
payload: this.peerConnection?.localDescription,
|
|
158
|
+
}));
|
|
159
|
+
this.wsConnection?.send(JSON.stringify({
|
|
160
|
+
type: "form",
|
|
161
|
+
payload: this.form,
|
|
162
|
+
}));
|
|
163
|
+
};
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error('Error starting conversation:', error);
|
|
168
|
+
this.connected = false;
|
|
169
|
+
this.wsConnection = null;
|
|
170
|
+
this.peerConnection = null;
|
|
171
|
+
this.localStream = null;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Stop the conversation, this will cut off audio mid output
|
|
177
|
+
*/
|
|
178
|
+
stop() {
|
|
179
|
+
console.log('Stop called');
|
|
180
|
+
if (!this.connected) {
|
|
181
|
+
console.error('Not connected to veform server');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.connected = false;
|
|
185
|
+
this.wsConnection?.close();
|
|
186
|
+
this.wsConnection = null;
|
|
187
|
+
this.peerConnection?.close();
|
|
188
|
+
this.peerConnection = null;
|
|
189
|
+
this.localStream?.getTracks().forEach((track) => {
|
|
190
|
+
track.stop();
|
|
191
|
+
});
|
|
192
|
+
this.localStream = null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Emit audio to client
|
|
196
|
+
* interrupt: if true, will interrupt the current output, otherwise will be queued behind current audio
|
|
197
|
+
*/
|
|
198
|
+
emitAudio(audio, interrupt = false) {
|
|
199
|
+
console.log('Emit audio called', audio);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Change the current field
|
|
203
|
+
* This will change the current field to the one provided
|
|
204
|
+
*/
|
|
205
|
+
changeField(fieldName, interrupt = false) {
|
|
206
|
+
console.log('Change field called', fieldName, interrupt);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Stop current audio output
|
|
210
|
+
*/
|
|
211
|
+
interrupt() {
|
|
212
|
+
console.log('Interrupt called');
|
|
213
|
+
}
|
|
214
|
+
resolveWsMessage(message) {
|
|
215
|
+
console.log('RAW WS MESSAGE:', message);
|
|
216
|
+
switch (message.type) {
|
|
217
|
+
case "event-start":
|
|
218
|
+
if (this.eventHandlers.onRunningStarted) {
|
|
219
|
+
this.eventHandlers.onRunningStarted();
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
case "event-end":
|
|
223
|
+
if (this.eventHandlers.onFinished) {
|
|
224
|
+
this.eventHandlers.onFinished();
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
case "event-audio-out-start":
|
|
228
|
+
if (this.eventHandlers.onAudioOutStart) {
|
|
229
|
+
const interrupt = this.eventHandlers.onAudioOutStart(message.payload);
|
|
230
|
+
if (interrupt) {
|
|
231
|
+
console.log('Audio out start interrupted');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
case "event-audio-out-end":
|
|
236
|
+
if (this.eventHandlers.onAudioOutEnd) {
|
|
237
|
+
this.eventHandlers.onAudioOutEnd();
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
case "event-input-start":
|
|
241
|
+
if (this.eventHandlers.onAudioInStart) {
|
|
242
|
+
this.eventHandlers.onAudioInStart();
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
case "event-input-end":
|
|
246
|
+
if (this.eventHandlers.onAudioInEnd) {
|
|
247
|
+
const interrupt = this.eventHandlers.onAudioInEnd(message.payload);
|
|
248
|
+
if (interrupt) {
|
|
249
|
+
console.log('Audio in end interrupted');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
case "event-focus-changed":
|
|
254
|
+
if (this.eventHandlers.onFocusChanged) {
|
|
255
|
+
const interrupt = this.eventHandlers.onFocusChanged(message.payload.previousName, message.payload.nextName);
|
|
256
|
+
if (interrupt) {
|
|
257
|
+
console.log('Focus changed interrupted');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const nextField = this.form.fields.find((field) => field.name === message.payload.nextName);
|
|
262
|
+
if (nextField?.eventHandlers?.onFocus) {
|
|
263
|
+
nextField.eventHandlers.onFocus(message.payload.previousName);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
case "event-field-value-changed":
|
|
267
|
+
console.log('CHECKING FOR FIELD:', message.payload);
|
|
268
|
+
const field = this.form.fields.find((field) => field.name === message.payload.fieldName);
|
|
269
|
+
console.log('FIELD:', field);
|
|
270
|
+
if (field?.eventHandlers?.onChange) {
|
|
271
|
+
field.eventHandlers.onChange(message.payload.value);
|
|
272
|
+
}
|
|
273
|
+
if (this.eventHandlers.onFieldValueChanged) {
|
|
274
|
+
this.eventHandlers.onFieldValueChanged(message.payload.fieldName, message.payload.value);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
case "event-error":
|
|
278
|
+
if (this.eventHandlers.onError) {
|
|
279
|
+
this.eventHandlers.onError(message.payload);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
case "event-critical-error":
|
|
283
|
+
if (this.eventHandlers.onCriticalError) {
|
|
284
|
+
this.eventHandlers.onCriticalError(message.payload);
|
|
285
|
+
this.stop();
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
default:
|
|
289
|
+
console.error('Unknown event type:', message.type);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function createAudioElement() {
|
|
295
|
+
const element = document.createElement('audio');
|
|
296
|
+
element.id = 'veform-audio';
|
|
297
|
+
element.style.opacity = '0';
|
|
298
|
+
element.style.position = 'absolute';
|
|
299
|
+
element.style.bottom = '0';
|
|
300
|
+
element.style.right = '0';
|
|
301
|
+
element.style.width = '100%';
|
|
302
|
+
element.style.height = '100%';
|
|
303
|
+
document.body.appendChild(element);
|
|
304
|
+
return element;
|
|
305
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "veform-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Veform JavaScript/TypeScript library",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && node build.js",
|
|
11
|
+
"prepare": "yarn build",
|
|
12
|
+
"watch": "tsc --watch"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"veform"
|
|
16
|
+
],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.3.3",
|
|
21
|
+
"esbuild": "^0.19.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
]
|
|
26
|
+
}
|