@vortexm/vjt 0.1.4 → 0.1.6
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/dist/index.d.ts +1 -1
- package/dist/index.js +1120 -106
- package/dist/lib/action-runtime.d.ts +33 -0
- package/dist/lib/dom-state.d.ts +6 -2
- package/dist/lib/network.d.ts +2 -0
- package/dist/lib/references.d.ts +2 -0
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/voice-runtime.d.ts +48 -0
- package/dist/lib/widgets/confirm-modal.d.ts +7 -0
- package/dist/lib/widgets/context-menu.d.ts +12 -6
- package/dist/lib/widgets/context.d.ts +2 -6
- package/dist/lib/widgets/delegation.d.ts +10 -0
- package/package.json +1 -1
- package/vjt-styles.css +63 -5
package/dist/index.js
CHANGED
|
@@ -3904,69 +3904,111 @@ var NetworkRuntime = class {
|
|
|
3904
3904
|
if (!definition) {
|
|
3905
3905
|
return null;
|
|
3906
3906
|
}
|
|
3907
|
-
const payload = await this.buildSchemaValue(definition.request, currentValue, void 0);
|
|
3908
|
-
const requestEnvelope = {
|
|
3909
|
-
jsonrpc: "2.0",
|
|
3910
|
-
id: Date.now(),
|
|
3911
|
-
method: requestName,
|
|
3912
|
-
params: payload
|
|
3913
|
-
};
|
|
3914
|
-
const requestUrl = definition.url ?? this.backendUrl;
|
|
3915
|
-
if (!requestUrl) {
|
|
3916
|
-
throw new Error(`Request ${requestName} has no url in config`);
|
|
3917
|
-
}
|
|
3918
|
-
const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
|
|
3919
|
-
if (!safeRequestUrl) {
|
|
3920
|
-
throw new Error(`Request ${requestName} has unsafe url`);
|
|
3921
|
-
}
|
|
3922
|
-
const requestBody = JSON.stringify(requestEnvelope);
|
|
3923
|
-
logRuntimeDebug(this.debugLogging, "request", {
|
|
3924
|
-
requestName,
|
|
3925
|
-
url: safeRequestUrl,
|
|
3926
|
-
envelope: requestEnvelope,
|
|
3927
|
-
body: requestBody
|
|
3928
|
-
});
|
|
3929
|
-
const response = await fetch(safeRequestUrl, {
|
|
3930
|
-
method: "POST",
|
|
3931
|
-
headers: {
|
|
3932
|
-
"Content-Type": "application/json"
|
|
3933
|
-
},
|
|
3934
|
-
body: requestBody
|
|
3935
|
-
});
|
|
3936
|
-
const responseText = await response.text();
|
|
3937
|
-
if (!response.ok) {
|
|
3938
|
-
throw new Error(`Request ${requestName} failed: ${response.status} ${response.statusText || responseText}`);
|
|
3939
|
-
}
|
|
3940
|
-
if (!responseText.trim()) {
|
|
3941
|
-
throw new Error(`Request ${requestName} returned empty response body`);
|
|
3942
|
-
}
|
|
3943
|
-
let json;
|
|
3944
3907
|
try {
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3908
|
+
const payload = await this.buildSchemaValue(definition.request, currentValue, void 0);
|
|
3909
|
+
const requestEnvelope = {
|
|
3910
|
+
jsonrpc: "2.0",
|
|
3911
|
+
id: Date.now(),
|
|
3912
|
+
method: requestName,
|
|
3913
|
+
params: payload
|
|
3914
|
+
};
|
|
3915
|
+
const requestUrl = definition.url ?? this.backendUrl;
|
|
3916
|
+
if (!requestUrl) {
|
|
3917
|
+
throw new Error(`Request ${requestName} has no url in config`);
|
|
3918
|
+
}
|
|
3919
|
+
const safeRequestUrl = sanitizeUrl(requestUrl, { allowRelative: true });
|
|
3920
|
+
if (!safeRequestUrl) {
|
|
3921
|
+
throw new Error(`Request ${requestName} has unsafe url`);
|
|
3922
|
+
}
|
|
3923
|
+
const requestBody = JSON.stringify(requestEnvelope);
|
|
3924
|
+
logRuntimeDebug(this.debugLogging, "request", {
|
|
3925
|
+
requestName,
|
|
3926
|
+
url: safeRequestUrl,
|
|
3927
|
+
envelope: requestEnvelope,
|
|
3928
|
+
body: requestBody
|
|
3929
|
+
});
|
|
3930
|
+
const response = await fetch(safeRequestUrl, {
|
|
3931
|
+
method: "POST",
|
|
3932
|
+
headers: {
|
|
3933
|
+
"Content-Type": "application/json"
|
|
3934
|
+
},
|
|
3935
|
+
body: requestBody
|
|
3967
3936
|
});
|
|
3937
|
+
const responseText = await response.text();
|
|
3938
|
+
if (!response.ok) {
|
|
3939
|
+
throw this.createRequestError(requestName, {
|
|
3940
|
+
message: `Request ${requestName} failed: ${response.status} ${response.statusText || responseText}`,
|
|
3941
|
+
status: response.status,
|
|
3942
|
+
statusText: response.statusText,
|
|
3943
|
+
body: responseText,
|
|
3944
|
+
url: safeRequestUrl
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
if (!responseText.trim()) {
|
|
3948
|
+
throw this.createRequestError(requestName, {
|
|
3949
|
+
message: `Request ${requestName} returned empty response body`,
|
|
3950
|
+
status: response.status,
|
|
3951
|
+
statusText: response.statusText,
|
|
3952
|
+
body: responseText,
|
|
3953
|
+
url: safeRequestUrl
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
let json;
|
|
3957
|
+
try {
|
|
3958
|
+
json = JSON.parse(responseText);
|
|
3959
|
+
} catch {
|
|
3960
|
+
throw this.createRequestError(requestName, {
|
|
3961
|
+
message: `Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`,
|
|
3962
|
+
status: response.status,
|
|
3963
|
+
statusText: response.statusText,
|
|
3964
|
+
body: responseText,
|
|
3965
|
+
url: safeRequestUrl
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
if (json.error) {
|
|
3969
|
+
throw this.createRequestError(requestName, {
|
|
3970
|
+
message: `Request ${requestName} returned JSON-RPC error: ${this.stringifyPrimitive(json.error.message ?? "unknown error")}`,
|
|
3971
|
+
status: response.status,
|
|
3972
|
+
statusText: response.statusText,
|
|
3973
|
+
body: responseText,
|
|
3974
|
+
url: safeRequestUrl,
|
|
3975
|
+
error: json.error
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
if (!definition.response) {
|
|
3979
|
+
throw this.createRequestError(requestName, {
|
|
3980
|
+
message: `Request ${requestName} must define response schema`,
|
|
3981
|
+
status: response.status,
|
|
3982
|
+
statusText: response.statusText,
|
|
3983
|
+
body: responseText,
|
|
3984
|
+
url: safeRequestUrl
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
|
|
3988
|
+
logRuntimeDebug(this.debugLogging, "response", {
|
|
3989
|
+
requestName,
|
|
3990
|
+
url: safeRequestUrl,
|
|
3991
|
+
status: response.status,
|
|
3992
|
+
json,
|
|
3993
|
+
result
|
|
3994
|
+
});
|
|
3995
|
+
if (definition.onResponse?.length) {
|
|
3996
|
+
await this.runActions(definition.onResponse, null, {
|
|
3997
|
+
currentValue: null,
|
|
3998
|
+
responseValue: result
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
return result;
|
|
4002
|
+
} catch (error) {
|
|
4003
|
+
if (definition.onError?.length) {
|
|
4004
|
+
const errorPayload = this.toRequestErrorPayload(requestName, error);
|
|
4005
|
+
await this.runActions(definition.onError, null, {
|
|
4006
|
+
currentValue: null,
|
|
4007
|
+
responseValue: errorPayload
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
throw error;
|
|
3968
4011
|
}
|
|
3969
|
-
return result;
|
|
3970
4012
|
}
|
|
3971
4013
|
async buildSchemaValue(schema, currentValue, responseValue) {
|
|
3972
4014
|
if (schema.type === "object") {
|
|
@@ -4004,6 +4046,36 @@ var NetworkRuntime = class {
|
|
|
4004
4046
|
}
|
|
4005
4047
|
return JSON.stringify(value);
|
|
4006
4048
|
}
|
|
4049
|
+
createRequestError(requestName, payload) {
|
|
4050
|
+
const error = new Error(payload.message);
|
|
4051
|
+
error.requestPayload = {
|
|
4052
|
+
requestName,
|
|
4053
|
+
message: payload.message,
|
|
4054
|
+
status: payload.status ?? null,
|
|
4055
|
+
statusText: payload.statusText ?? "",
|
|
4056
|
+
body: payload.body ?? "",
|
|
4057
|
+
url: payload.url ?? "",
|
|
4058
|
+
error: payload.error ?? null
|
|
4059
|
+
};
|
|
4060
|
+
return error;
|
|
4061
|
+
}
|
|
4062
|
+
toRequestErrorPayload(requestName, error) {
|
|
4063
|
+
if (typeof error === "object" && error !== null && "requestPayload" in error) {
|
|
4064
|
+
const payload = error.requestPayload;
|
|
4065
|
+
if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
|
|
4066
|
+
return payload;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
return {
|
|
4070
|
+
requestName,
|
|
4071
|
+
message: error instanceof Error ? error.message : this.stringifyPrimitive(error),
|
|
4072
|
+
status: null,
|
|
4073
|
+
statusText: "",
|
|
4074
|
+
body: "",
|
|
4075
|
+
url: "",
|
|
4076
|
+
error: null
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4007
4079
|
coercePrimitiveSchemaValue(type, value) {
|
|
4008
4080
|
switch (type) {
|
|
4009
4081
|
case "int": {
|
|
@@ -4186,6 +4258,9 @@ var ReferenceRuntime = class {
|
|
|
4186
4258
|
if (reference.startsWith("url.")) {
|
|
4187
4259
|
return getUrlParamValue(reference.slice(4));
|
|
4188
4260
|
}
|
|
4261
|
+
if (reference.startsWith("vars.")) {
|
|
4262
|
+
return this.host.getVarValue(reference.slice(5));
|
|
4263
|
+
}
|
|
4189
4264
|
const [widgetId, ...rest] = reference.split(".");
|
|
4190
4265
|
const state = this.host.stateById.get(widgetId);
|
|
4191
4266
|
if (!state) {
|
|
@@ -4311,6 +4386,10 @@ var ReferenceRuntime = class {
|
|
|
4311
4386
|
this.host.setAppValue(reference.slice(4), inputValue);
|
|
4312
4387
|
return;
|
|
4313
4388
|
}
|
|
4389
|
+
if (reference.startsWith("vars.")) {
|
|
4390
|
+
this.host.setVarValue(reference.slice(5), inputValue);
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4314
4393
|
const [widgetId, ...rest] = reference.split(".");
|
|
4315
4394
|
const field = rest.join(".");
|
|
4316
4395
|
const state = this.host.stateById.get(widgetId);
|
|
@@ -4489,6 +4568,7 @@ var ActionRuntime = class {
|
|
|
4489
4568
|
actionFunctions;
|
|
4490
4569
|
nodeById;
|
|
4491
4570
|
stateById;
|
|
4571
|
+
stateByKey;
|
|
4492
4572
|
rerenderRoot;
|
|
4493
4573
|
dispatchWidgetEventImpl;
|
|
4494
4574
|
executeRequest;
|
|
@@ -4498,6 +4578,17 @@ var ActionRuntime = class {
|
|
|
4498
4578
|
resolveMappedValue;
|
|
4499
4579
|
setWidgetEnabled;
|
|
4500
4580
|
clearWidget;
|
|
4581
|
+
clearListElementState;
|
|
4582
|
+
focusWidget;
|
|
4583
|
+
playAudio;
|
|
4584
|
+
stopPlaying;
|
|
4585
|
+
copyToClipboard;
|
|
4586
|
+
selectFile;
|
|
4587
|
+
confirmModal;
|
|
4588
|
+
startRecording;
|
|
4589
|
+
stopRecording;
|
|
4590
|
+
startListening;
|
|
4591
|
+
stopListening;
|
|
4501
4592
|
setUiReference;
|
|
4502
4593
|
refreshWidgetTree;
|
|
4503
4594
|
renderWidgetTree;
|
|
@@ -4514,6 +4605,7 @@ var ActionRuntime = class {
|
|
|
4514
4605
|
this.actionFunctions = options.actionFunctions;
|
|
4515
4606
|
this.nodeById = options.nodeById;
|
|
4516
4607
|
this.stateById = options.stateById;
|
|
4608
|
+
this.stateByKey = options.stateByKey;
|
|
4517
4609
|
this.rerenderRoot = options.rerenderRoot;
|
|
4518
4610
|
this.dispatchWidgetEventImpl = options.dispatchWidgetEvent;
|
|
4519
4611
|
this.executeRequest = options.executeRequest;
|
|
@@ -4523,6 +4615,17 @@ var ActionRuntime = class {
|
|
|
4523
4615
|
this.resolveMappedValue = options.resolveMappedValue;
|
|
4524
4616
|
this.setWidgetEnabled = options.setWidgetEnabled;
|
|
4525
4617
|
this.clearWidget = options.clearWidget;
|
|
4618
|
+
this.clearListElementState = options.clearListElementState;
|
|
4619
|
+
this.focusWidget = options.focusWidget;
|
|
4620
|
+
this.playAudio = options.playAudio;
|
|
4621
|
+
this.stopPlaying = options.stopPlaying;
|
|
4622
|
+
this.copyToClipboard = options.copyToClipboard;
|
|
4623
|
+
this.selectFile = options.selectFile;
|
|
4624
|
+
this.confirmModal = options.confirmModal;
|
|
4625
|
+
this.startRecording = options.startRecording;
|
|
4626
|
+
this.stopRecording = options.stopRecording;
|
|
4627
|
+
this.startListening = options.startListening;
|
|
4628
|
+
this.stopListening = options.stopListening;
|
|
4526
4629
|
this.setUiReference = options.setUiReference;
|
|
4527
4630
|
this.refreshWidgetTree = options.refreshWidgetTree;
|
|
4528
4631
|
this.renderWidgetTree = options.renderWidgetTree;
|
|
@@ -4557,7 +4660,7 @@ var ActionRuntime = class {
|
|
|
4557
4660
|
async runActions(actions, inputValue, context) {
|
|
4558
4661
|
let current = inputValue;
|
|
4559
4662
|
for (const action of actions) {
|
|
4560
|
-
current = await this.runSingleAction(action,
|
|
4663
|
+
current = await this.runSingleAction(action, inputValue, context);
|
|
4561
4664
|
}
|
|
4562
4665
|
return current;
|
|
4563
4666
|
}
|
|
@@ -4569,19 +4672,7 @@ var ActionRuntime = class {
|
|
|
4569
4672
|
try {
|
|
4570
4673
|
let output = await this.executeAction(action, inputValue, context);
|
|
4571
4674
|
if (action.andThen?.length) {
|
|
4572
|
-
if (
|
|
4573
|
-
const collected = [];
|
|
4574
|
-
for (const entry of output) {
|
|
4575
|
-
if (entry === null || entry === void 0) {
|
|
4576
|
-
continue;
|
|
4577
|
-
}
|
|
4578
|
-
const nestedOutput = await this.runActions(action.andThen, entry, { ...context, currentValue: entry });
|
|
4579
|
-
if (nestedOutput !== null && nestedOutput !== void 0) {
|
|
4580
|
-
collected.push(nestedOutput);
|
|
4581
|
-
}
|
|
4582
|
-
}
|
|
4583
|
-
output = collected;
|
|
4584
|
-
} else if (output !== null && output !== void 0) {
|
|
4675
|
+
if (output !== null && output !== void 0) {
|
|
4585
4676
|
output = await this.runActions(action.andThen, output, { ...context, currentValue: output });
|
|
4586
4677
|
} else {
|
|
4587
4678
|
output = null;
|
|
@@ -4641,6 +4732,40 @@ var ActionRuntime = class {
|
|
|
4641
4732
|
await this.navigateTo(name.slice(9));
|
|
4642
4733
|
return null;
|
|
4643
4734
|
}
|
|
4735
|
+
if (name.startsWith("play ")) {
|
|
4736
|
+
await this.playAudio(this.resolveReference(name.slice(5), context.currentValue, context.responseValue));
|
|
4737
|
+
return null;
|
|
4738
|
+
}
|
|
4739
|
+
if (name === "copyToClipboard") {
|
|
4740
|
+
await this.copyToClipboard(inputValue);
|
|
4741
|
+
return inputValue;
|
|
4742
|
+
}
|
|
4743
|
+
if (name === "selectFile") {
|
|
4744
|
+
return this.selectFile(action.args);
|
|
4745
|
+
}
|
|
4746
|
+
if (name === "confirmModal") {
|
|
4747
|
+
return await this.confirmModal(action.args, inputValue) ? inputValue : void 0;
|
|
4748
|
+
}
|
|
4749
|
+
if (name === "stopPlaying") {
|
|
4750
|
+
await this.stopPlaying();
|
|
4751
|
+
return null;
|
|
4752
|
+
}
|
|
4753
|
+
if (name === "startRecording") {
|
|
4754
|
+
await this.startRecording();
|
|
4755
|
+
return null;
|
|
4756
|
+
}
|
|
4757
|
+
if (name === "stopRecording") {
|
|
4758
|
+
await this.stopRecording();
|
|
4759
|
+
return null;
|
|
4760
|
+
}
|
|
4761
|
+
if (name === "startListening") {
|
|
4762
|
+
await this.startListening();
|
|
4763
|
+
return null;
|
|
4764
|
+
}
|
|
4765
|
+
if (name === "stopListening") {
|
|
4766
|
+
await this.stopListening();
|
|
4767
|
+
return null;
|
|
4768
|
+
}
|
|
4644
4769
|
if (name.startsWith("showMenu ")) {
|
|
4645
4770
|
const menuId = name.slice(9);
|
|
4646
4771
|
const menuState = this.stateById.get(menuId);
|
|
@@ -4651,7 +4776,9 @@ var ActionRuntime = class {
|
|
|
4651
4776
|
this.setActiveContextMenu({
|
|
4652
4777
|
id: menuId,
|
|
4653
4778
|
x: position.x,
|
|
4654
|
-
y: position.y
|
|
4779
|
+
y: position.y,
|
|
4780
|
+
currentValue: context.currentValue,
|
|
4781
|
+
openPath: []
|
|
4655
4782
|
});
|
|
4656
4783
|
return null;
|
|
4657
4784
|
}
|
|
@@ -4680,6 +4807,10 @@ var ActionRuntime = class {
|
|
|
4680
4807
|
this.clearWidget(name.slice(6));
|
|
4681
4808
|
return null;
|
|
4682
4809
|
}
|
|
4810
|
+
if (name.startsWith("setFocus ")) {
|
|
4811
|
+
this.focusWidget(name.slice(9));
|
|
4812
|
+
return null;
|
|
4813
|
+
}
|
|
4683
4814
|
if (name.startsWith("setUi ")) {
|
|
4684
4815
|
const widgetId = name.slice(6);
|
|
4685
4816
|
const resourceRef = typeof inputValue === "string" ? inputValue : "";
|
|
@@ -4689,7 +4820,11 @@ var ActionRuntime = class {
|
|
|
4689
4820
|
return inputValue;
|
|
4690
4821
|
}
|
|
4691
4822
|
if (name.startsWith("get ")) {
|
|
4692
|
-
|
|
4823
|
+
const reference = name.slice(4);
|
|
4824
|
+
if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
|
|
4825
|
+
return context.currentValue.map((entry) => this.resolveReference(reference, entry, context.responseValue)).filter((entry) => entry !== null && entry !== void 0);
|
|
4826
|
+
}
|
|
4827
|
+
return this.resolveReference(reference, context.currentValue, context.responseValue);
|
|
4693
4828
|
}
|
|
4694
4829
|
if (name.startsWith("equals ")) {
|
|
4695
4830
|
return this.resolveReference(name.slice(7), context.currentValue, context.responseValue) === action.args;
|
|
@@ -4702,11 +4837,82 @@ var ActionRuntime = class {
|
|
|
4702
4837
|
this.assignReference(name.slice(4), inputValue, context.currentValue, args);
|
|
4703
4838
|
return inputValue;
|
|
4704
4839
|
}
|
|
4840
|
+
if (name.startsWith("append ")) {
|
|
4841
|
+
const reference = name.slice(7);
|
|
4842
|
+
const currentText = this.resolveReference(reference, context.currentValue, context.responseValue);
|
|
4843
|
+
const nextValue = `${this.stringifyValue(currentText)}${this.stringifyValue(action.args)}`;
|
|
4844
|
+
this.assignReference(reference, nextValue, context.currentValue);
|
|
4845
|
+
return nextValue;
|
|
4846
|
+
}
|
|
4705
4847
|
if (name.startsWith("filter ")) {
|
|
4706
|
-
const
|
|
4848
|
+
const reference = name.slice(7);
|
|
4849
|
+
if (Array.isArray(context.currentValue) && this.isCurrentScopedReference(reference)) {
|
|
4850
|
+
return context.currentValue.filter((entry) => this.resolveReference(reference, entry, context.responseValue));
|
|
4851
|
+
}
|
|
4852
|
+
const value = this.resolveReference(reference, context.currentValue, context.responseValue);
|
|
4707
4853
|
return value ? inputValue : null;
|
|
4708
4854
|
}
|
|
4855
|
+
if (name === "remove") {
|
|
4856
|
+
const listState = this.getCurrentListState(context.currentValue);
|
|
4857
|
+
if (listState && typeof context.currentValue === "object" && context.currentValue !== null && "index" in context.currentValue) {
|
|
4858
|
+
const index = context.currentValue.index;
|
|
4859
|
+
if (typeof index === "number" && Array.isArray(listState.elements)) {
|
|
4860
|
+
listState.elements = listState.elements.filter((_, elementIndex) => elementIndex !== index);
|
|
4861
|
+
this.clearListElementState(listState.key);
|
|
4862
|
+
return listState.elements;
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
return null;
|
|
4866
|
+
}
|
|
4867
|
+
if (name.startsWith("add ")) {
|
|
4868
|
+
const valueToAdd = this.cloneValue(this.unwrapListValue(this.resolveReference(name.slice(4), context.currentValue, context.responseValue)));
|
|
4869
|
+
const listState = this.getCurrentListState(context.currentValue);
|
|
4870
|
+
if (listState && Array.isArray(listState.elements)) {
|
|
4871
|
+
listState.elements.push(valueToAdd);
|
|
4872
|
+
this.clearListElementState(listState.key);
|
|
4873
|
+
return listState.elements;
|
|
4874
|
+
}
|
|
4875
|
+
if (context.currentList && typeof context.currentIndex === "number") {
|
|
4876
|
+
return context.currentIndex === context.currentList.length - 1 ? [this.unwrapListValue(inputValue), valueToAdd] : this.unwrapListValue(inputValue);
|
|
4877
|
+
}
|
|
4878
|
+
if (Array.isArray(inputValue)) {
|
|
4879
|
+
const nextList = [];
|
|
4880
|
+
for (const item of inputValue) {
|
|
4881
|
+
nextList.push(item);
|
|
4882
|
+
}
|
|
4883
|
+
nextList.push(valueToAdd);
|
|
4884
|
+
return nextList;
|
|
4885
|
+
}
|
|
4886
|
+
return inputValue;
|
|
4887
|
+
}
|
|
4888
|
+
if (name.startsWith("insert ")) {
|
|
4889
|
+
const valueToInsert = this.cloneValue(this.unwrapListValue(this.resolveReference(name.slice(7), context.currentValue, context.responseValue)));
|
|
4890
|
+
const listState = this.getCurrentListState(context.currentValue);
|
|
4891
|
+
if (listState && Array.isArray(listState.elements) && typeof context.currentValue === "object" && context.currentValue !== null && "index" in context.currentValue) {
|
|
4892
|
+
const index = context.currentValue.index;
|
|
4893
|
+
if (typeof index === "number") {
|
|
4894
|
+
listState.elements.splice(index + 1, 0, valueToInsert);
|
|
4895
|
+
this.clearListElementState(listState.key);
|
|
4896
|
+
return listState.elements;
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
if (context.currentList && typeof context.currentIndex === "number") {
|
|
4900
|
+
return [this.unwrapListValue(inputValue), valueToInsert];
|
|
4901
|
+
}
|
|
4902
|
+
if (Array.isArray(inputValue)) {
|
|
4903
|
+
const nextList = [];
|
|
4904
|
+
for (const item of inputValue) {
|
|
4905
|
+
nextList.push(item);
|
|
4906
|
+
}
|
|
4907
|
+
nextList.push(valueToInsert);
|
|
4908
|
+
return nextList;
|
|
4909
|
+
}
|
|
4910
|
+
return inputValue;
|
|
4911
|
+
}
|
|
4709
4912
|
if (name === "map") {
|
|
4913
|
+
if (Array.isArray(context.currentValue)) {
|
|
4914
|
+
return context.currentValue.map((entry) => this.resolveMappedValue(action.args, entry, context.responseValue));
|
|
4915
|
+
}
|
|
4710
4916
|
return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
|
|
4711
4917
|
}
|
|
4712
4918
|
if (name === "ifelse") {
|
|
@@ -4722,10 +4928,43 @@ var ActionRuntime = class {
|
|
|
4722
4928
|
}
|
|
4723
4929
|
return inputValue;
|
|
4724
4930
|
}
|
|
4931
|
+
unwrapListValue(value) {
|
|
4932
|
+
return isListElementLike(value) ? value.descriptor : value;
|
|
4933
|
+
}
|
|
4934
|
+
getCurrentListState(currentValue) {
|
|
4935
|
+
if (!isListElementLike(currentValue) || typeof currentValue.listId !== "string") {
|
|
4936
|
+
return null;
|
|
4937
|
+
}
|
|
4938
|
+
return this.stateByKey.get(currentValue.listId) ?? this.stateById.get(currentValue.listId) ?? null;
|
|
4939
|
+
}
|
|
4940
|
+
cloneValue(value) {
|
|
4941
|
+
if (value === null || value === void 0) {
|
|
4942
|
+
return value;
|
|
4943
|
+
}
|
|
4944
|
+
if (typeof value === "object") {
|
|
4945
|
+
return structuredClone(value);
|
|
4946
|
+
}
|
|
4947
|
+
return value;
|
|
4948
|
+
}
|
|
4949
|
+
stringifyValue(value) {
|
|
4950
|
+
if (value == null) {
|
|
4951
|
+
return "";
|
|
4952
|
+
}
|
|
4953
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
4954
|
+
return String(value);
|
|
4955
|
+
}
|
|
4956
|
+
return JSON.stringify(value);
|
|
4957
|
+
}
|
|
4958
|
+
isCurrentScopedReference(reference) {
|
|
4959
|
+
return reference === "current" || reference.startsWith("current.") || reference.startsWith("this.") || reference === "this";
|
|
4960
|
+
}
|
|
4725
4961
|
};
|
|
4726
4962
|
function isIfElseArgs(value) {
|
|
4727
4963
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4728
4964
|
}
|
|
4965
|
+
function isListElementLike(value) {
|
|
4966
|
+
return typeof value === "object" && value !== null && "kind" in value && value.kind === "list-element" && "listId" in value && typeof value.listId === "string" && "index" in value && typeof value.index === "number" && "descriptor" in value;
|
|
4967
|
+
}
|
|
4729
4968
|
|
|
4730
4969
|
// src/lib/dom-state.ts
|
|
4731
4970
|
function isPlainObject3(value) {
|
|
@@ -4754,7 +4993,7 @@ function isElementInsidePendingResetModal(element, pendingResetModalIds) {
|
|
|
4754
4993
|
}
|
|
4755
4994
|
return false;
|
|
4756
4995
|
}
|
|
4757
|
-
function captureElementState(root, pendingResetModalIds
|
|
4996
|
+
function captureElementState(root, pendingResetModalIds) {
|
|
4758
4997
|
const state = {
|
|
4759
4998
|
activeId: null,
|
|
4760
4999
|
values: /* @__PURE__ */ new Map(),
|
|
@@ -4782,20 +5021,6 @@ function captureElementState(root, pendingResetModalIds, stateByKey) {
|
|
|
4782
5021
|
if (element instanceof HTMLInputElement && element.type === "checkbox" && element.id) {
|
|
4783
5022
|
state.checked.set(element.id, element.checked);
|
|
4784
5023
|
}
|
|
4785
|
-
const widgetKey = element.dataset.widgetKey;
|
|
4786
|
-
if (!widgetKey) {
|
|
4787
|
-
continue;
|
|
4788
|
-
}
|
|
4789
|
-
const widgetState = stateByKey.get(widgetKey);
|
|
4790
|
-
if (!widgetState || element.readOnly || element.disabled) {
|
|
4791
|
-
continue;
|
|
4792
|
-
}
|
|
4793
|
-
if (widgetState.widget === "edit" || widgetState.widget === "textarea") {
|
|
4794
|
-
widgetState.text = element.value;
|
|
4795
|
-
}
|
|
4796
|
-
if (widgetState.widget === "checkbox" && element instanceof HTMLInputElement && element.type === "checkbox") {
|
|
4797
|
-
widgetState.checked = element.checked;
|
|
4798
|
-
}
|
|
4799
5024
|
}
|
|
4800
5025
|
for (const element of Array.from(root.querySelectorAll("[id], [data-widget-key]"))) {
|
|
4801
5026
|
const top = element.scrollTop;
|
|
@@ -4817,16 +5042,44 @@ function captureElementState(root, pendingResetModalIds, stateByKey) {
|
|
|
4817
5042
|
}
|
|
4818
5043
|
return state;
|
|
4819
5044
|
}
|
|
4820
|
-
function
|
|
5045
|
+
function shouldRestoreTextValue(element, preservedValue, stateByKey) {
|
|
5046
|
+
const widgetKey = element.dataset.widgetKey;
|
|
5047
|
+
if (!widgetKey) {
|
|
5048
|
+
return true;
|
|
5049
|
+
}
|
|
5050
|
+
const widgetState = stateByKey.get(widgetKey);
|
|
5051
|
+
if (!widgetState || widgetState.widget !== "edit" && widgetState.widget !== "textarea") {
|
|
5052
|
+
return true;
|
|
5053
|
+
}
|
|
5054
|
+
return (widgetState.text ?? "") === preservedValue;
|
|
5055
|
+
}
|
|
5056
|
+
function shouldRestoreCheckedValue(element, preservedValue, stateByKey) {
|
|
5057
|
+
const widgetKey = element.dataset.widgetKey;
|
|
5058
|
+
if (!widgetKey) {
|
|
5059
|
+
return true;
|
|
5060
|
+
}
|
|
5061
|
+
const widgetState = stateByKey.get(widgetKey);
|
|
5062
|
+
if (!widgetState || widgetState.widget !== "checkbox") {
|
|
5063
|
+
return true;
|
|
5064
|
+
}
|
|
5065
|
+
return (widgetState.checked ?? false) === preservedValue;
|
|
5066
|
+
}
|
|
5067
|
+
function restoreElementState(root, state, stateByKey) {
|
|
4821
5068
|
for (const [id, value] of state.values.entries()) {
|
|
4822
5069
|
const element = root.querySelector(`#${CSS.escape(id)}`);
|
|
4823
5070
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
5071
|
+
if (!shouldRestoreTextValue(element, value, stateByKey)) {
|
|
5072
|
+
continue;
|
|
5073
|
+
}
|
|
4824
5074
|
element.value = value;
|
|
4825
5075
|
}
|
|
4826
5076
|
}
|
|
4827
5077
|
for (const [id, checked] of state.checked.entries()) {
|
|
4828
5078
|
const element = root.querySelector(`#${CSS.escape(id)}`);
|
|
4829
5079
|
if (element instanceof HTMLInputElement && element.type === "checkbox") {
|
|
5080
|
+
if (!shouldRestoreCheckedValue(element, checked, stateByKey)) {
|
|
5081
|
+
continue;
|
|
5082
|
+
}
|
|
4830
5083
|
element.checked = checked;
|
|
4831
5084
|
}
|
|
4832
5085
|
}
|
|
@@ -5129,6 +5382,430 @@ var DEFAULT_STYLE_MAP = {
|
|
|
5129
5382
|
employeeLinkDark: "display:flex; align-items:center; width:100%; height:100%; color:#b8d0ea; font-weight:600;"
|
|
5130
5383
|
};
|
|
5131
5384
|
|
|
5385
|
+
// src/lib/voice-runtime.ts
|
|
5386
|
+
var MAX_CONCURRENT_PLAYERS = 5;
|
|
5387
|
+
var MAX_RECORDING_MS = 10 * 60 * 1e3;
|
|
5388
|
+
var MAX_LISTENING_SILENCE_MS = 60 * 60 * 1e3;
|
|
5389
|
+
var SILENCE_STOP_MS = 2e3;
|
|
5390
|
+
var SPEECH_THRESHOLD = 0.035;
|
|
5391
|
+
var RECORDING_MIME_CANDIDATES = [
|
|
5392
|
+
"audio/webm;codecs=opus",
|
|
5393
|
+
"audio/webm",
|
|
5394
|
+
"audio/mp4",
|
|
5395
|
+
"audio/mp4;codecs=mp4a.40.2",
|
|
5396
|
+
"audio/wav"
|
|
5397
|
+
];
|
|
5398
|
+
function isPlayableAudioPayload(value) {
|
|
5399
|
+
return typeof value === "object" && value !== null;
|
|
5400
|
+
}
|
|
5401
|
+
function decodeBase64(base64) {
|
|
5402
|
+
const normalized = base64.includes(",") ? base64.slice(base64.indexOf(",") + 1) : base64;
|
|
5403
|
+
const binary = atob(normalized);
|
|
5404
|
+
const bytes = new Uint8Array(binary.length);
|
|
5405
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
5406
|
+
bytes[index] = binary.charCodeAt(index);
|
|
5407
|
+
}
|
|
5408
|
+
return bytes;
|
|
5409
|
+
}
|
|
5410
|
+
function detectMimeType(bytes) {
|
|
5411
|
+
if (bytes.length >= 4 && bytes[0] === 26 && bytes[1] === 69 && bytes[2] === 223 && bytes[3] === 163) {
|
|
5412
|
+
return "audio/webm";
|
|
5413
|
+
}
|
|
5414
|
+
if (bytes.length >= 12 && bytes[4] === 102 && bytes[5] === 116 && bytes[6] === 121 && bytes[7] === 112) {
|
|
5415
|
+
return "audio/mp4";
|
|
5416
|
+
}
|
|
5417
|
+
if (bytes.length >= 3 && bytes[0] === 73 && bytes[1] === 68 && bytes[2] === 51) {
|
|
5418
|
+
return "audio/mpeg";
|
|
5419
|
+
}
|
|
5420
|
+
if (bytes.length >= 2 && bytes[0] === 255 && (bytes[1] & 224) === 224) {
|
|
5421
|
+
return "audio/mpeg";
|
|
5422
|
+
}
|
|
5423
|
+
if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 65 && bytes[10] === 86 && bytes[11] === 69) {
|
|
5424
|
+
return "audio/wav";
|
|
5425
|
+
}
|
|
5426
|
+
if (bytes.length >= 4 && bytes[0] === 79 && bytes[1] === 103 && bytes[2] === 103 && bytes[3] === 83) {
|
|
5427
|
+
return "audio/ogg";
|
|
5428
|
+
}
|
|
5429
|
+
return "audio/mpeg";
|
|
5430
|
+
}
|
|
5431
|
+
function blobToBase64(blob) {
|
|
5432
|
+
return new Promise((resolve, reject) => {
|
|
5433
|
+
const reader = new FileReader();
|
|
5434
|
+
reader.onerror = () => reject(reader.error ?? new Error("Failed to read recorded audio data"));
|
|
5435
|
+
reader.onloadend = () => {
|
|
5436
|
+
const result = typeof reader.result === "string" ? reader.result : "";
|
|
5437
|
+
const base64 = result.includes(",") ? result.slice(result.indexOf(",") + 1) : result;
|
|
5438
|
+
resolve(base64);
|
|
5439
|
+
};
|
|
5440
|
+
reader.readAsDataURL(blob);
|
|
5441
|
+
});
|
|
5442
|
+
}
|
|
5443
|
+
var VoiceRuntime = class {
|
|
5444
|
+
debugLogging;
|
|
5445
|
+
triggerSystemEvent;
|
|
5446
|
+
activePlayers = [];
|
|
5447
|
+
mediaStream = null;
|
|
5448
|
+
mediaRecorder = null;
|
|
5449
|
+
recordingChunks = [];
|
|
5450
|
+
recordingTimeoutId = null;
|
|
5451
|
+
listening = false;
|
|
5452
|
+
listenContext = null;
|
|
5453
|
+
listenAnalyser = null;
|
|
5454
|
+
listenSource = null;
|
|
5455
|
+
listenFrameId = null;
|
|
5456
|
+
lastSpeechAt = 0;
|
|
5457
|
+
listeningStartedAt = 0;
|
|
5458
|
+
recordingStartedFromListening = false;
|
|
5459
|
+
speechEventTriggered = false;
|
|
5460
|
+
visibilityHandler;
|
|
5461
|
+
pageHideHandler;
|
|
5462
|
+
constructor(options) {
|
|
5463
|
+
this.debugLogging = options.debugLogging;
|
|
5464
|
+
this.triggerSystemEvent = options.triggerSystemEvent;
|
|
5465
|
+
this.visibilityHandler = () => {
|
|
5466
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
5467
|
+
void this.handlePageDeactivation();
|
|
5468
|
+
}
|
|
5469
|
+
};
|
|
5470
|
+
this.pageHideHandler = () => {
|
|
5471
|
+
void this.handlePageDeactivation();
|
|
5472
|
+
};
|
|
5473
|
+
if (typeof document !== "undefined") {
|
|
5474
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
5475
|
+
}
|
|
5476
|
+
if (typeof window !== "undefined") {
|
|
5477
|
+
window.addEventListener("pagehide", this.pageHideHandler);
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
async play(value) {
|
|
5481
|
+
const payload = this.normalizePlayablePayload(value);
|
|
5482
|
+
if (!payload) {
|
|
5483
|
+
return;
|
|
5484
|
+
}
|
|
5485
|
+
const bytes = decodeBase64(payload.audioData);
|
|
5486
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
5487
|
+
copy.set(bytes);
|
|
5488
|
+
const blob = new Blob([copy.buffer], { type: payload.mimeType });
|
|
5489
|
+
const url = URL.createObjectURL(blob);
|
|
5490
|
+
const audio = new Audio(url);
|
|
5491
|
+
audio.preload = "auto";
|
|
5492
|
+
const player = { audio, url, payload };
|
|
5493
|
+
audio.onended = () => {
|
|
5494
|
+
void this.finishPlayback(player, "finished");
|
|
5495
|
+
};
|
|
5496
|
+
audio.onerror = () => {
|
|
5497
|
+
this.detachPlayer(player);
|
|
5498
|
+
logRuntimeError("voice.play", new Error("Audio playback failed"), { mimeType: payload.mimeType });
|
|
5499
|
+
};
|
|
5500
|
+
this.activePlayers.push(player);
|
|
5501
|
+
while (this.activePlayers.length > MAX_CONCURRENT_PLAYERS) {
|
|
5502
|
+
const oldest = this.activePlayers.shift();
|
|
5503
|
+
if (!oldest) {
|
|
5504
|
+
break;
|
|
5505
|
+
}
|
|
5506
|
+
oldest.audio.pause();
|
|
5507
|
+
void this.finishPlayback(oldest, "stopped");
|
|
5508
|
+
}
|
|
5509
|
+
logRuntimeDebug(this.debugLogging, "voice-play", {
|
|
5510
|
+
mimeType: payload.mimeType,
|
|
5511
|
+
bytes: bytes.byteLength,
|
|
5512
|
+
activePlayers: this.activePlayers.length
|
|
5513
|
+
});
|
|
5514
|
+
await audio.play();
|
|
5515
|
+
}
|
|
5516
|
+
async stopPlaying() {
|
|
5517
|
+
const players = [...this.activePlayers];
|
|
5518
|
+
for (const player of players) {
|
|
5519
|
+
player.audio.pause();
|
|
5520
|
+
player.audio.currentTime = 0;
|
|
5521
|
+
await this.finishPlayback(player, "stopped");
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
async startRecording() {
|
|
5525
|
+
try {
|
|
5526
|
+
const stream = await this.ensureInputStream();
|
|
5527
|
+
this.startRecordingInternal(stream, false);
|
|
5528
|
+
} catch (error) {
|
|
5529
|
+
await this.handleRecordingError(error);
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
stopRecording() {
|
|
5533
|
+
if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
|
|
5534
|
+
return;
|
|
5535
|
+
}
|
|
5536
|
+
this.clearRecordingTimeout();
|
|
5537
|
+
this.mediaRecorder.stop();
|
|
5538
|
+
}
|
|
5539
|
+
async startListening() {
|
|
5540
|
+
if (this.listening) {
|
|
5541
|
+
return;
|
|
5542
|
+
}
|
|
5543
|
+
try {
|
|
5544
|
+
const stream = await this.ensureInputStream();
|
|
5545
|
+
const context = new AudioContext();
|
|
5546
|
+
const analyser = context.createAnalyser();
|
|
5547
|
+
analyser.fftSize = 2048;
|
|
5548
|
+
const source = context.createMediaStreamSource(stream);
|
|
5549
|
+
source.connect(analyser);
|
|
5550
|
+
this.listening = true;
|
|
5551
|
+
this.listenContext = context;
|
|
5552
|
+
this.listenAnalyser = analyser;
|
|
5553
|
+
this.listenSource = source;
|
|
5554
|
+
this.lastSpeechAt = 0;
|
|
5555
|
+
this.listeningStartedAt = performance.now();
|
|
5556
|
+
this.speechEventTriggered = false;
|
|
5557
|
+
const sampleBuffer = new Uint8Array(analyser.fftSize);
|
|
5558
|
+
const step = async () => {
|
|
5559
|
+
if (!this.listening || !this.listenAnalyser) {
|
|
5560
|
+
return;
|
|
5561
|
+
}
|
|
5562
|
+
this.listenAnalyser.getByteTimeDomainData(sampleBuffer);
|
|
5563
|
+
let squareSum = 0;
|
|
5564
|
+
for (const sample of sampleBuffer) {
|
|
5565
|
+
const centered = (sample - 128) / 128;
|
|
5566
|
+
squareSum += centered * centered;
|
|
5567
|
+
}
|
|
5568
|
+
const rms = Math.sqrt(squareSum / sampleBuffer.length);
|
|
5569
|
+
const now = performance.now();
|
|
5570
|
+
if (rms >= SPEECH_THRESHOLD) {
|
|
5571
|
+
this.lastSpeechAt = now;
|
|
5572
|
+
this.listeningStartedAt = now;
|
|
5573
|
+
if (!this.speechEventTriggered) {
|
|
5574
|
+
this.speechEventTriggered = true;
|
|
5575
|
+
await this.triggerSystemEvent("onSpeechDetected", null);
|
|
5576
|
+
}
|
|
5577
|
+
if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
|
|
5578
|
+
this.startRecordingInternal(stream, true);
|
|
5579
|
+
}
|
|
5580
|
+
} else if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive" && this.lastSpeechAt > 0 && now - this.lastSpeechAt >= SILENCE_STOP_MS) {
|
|
5581
|
+
this.stopRecording();
|
|
5582
|
+
this.lastSpeechAt = 0;
|
|
5583
|
+
this.speechEventTriggered = false;
|
|
5584
|
+
} else if (!this.recordingStartedFromListening && now - this.listeningStartedAt >= MAX_LISTENING_SILENCE_MS) {
|
|
5585
|
+
await this.stopListening();
|
|
5586
|
+
return;
|
|
5587
|
+
}
|
|
5588
|
+
this.listenFrameId = window.requestAnimationFrame(() => {
|
|
5589
|
+
void step();
|
|
5590
|
+
});
|
|
5591
|
+
};
|
|
5592
|
+
void step();
|
|
5593
|
+
} catch (error) {
|
|
5594
|
+
await this.handleListeningError(error);
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
async stopListening() {
|
|
5598
|
+
this.listening = false;
|
|
5599
|
+
this.speechEventTriggered = false;
|
|
5600
|
+
this.lastSpeechAt = 0;
|
|
5601
|
+
this.listeningStartedAt = 0;
|
|
5602
|
+
if (this.listenFrameId !== null) {
|
|
5603
|
+
window.cancelAnimationFrame(this.listenFrameId);
|
|
5604
|
+
this.listenFrameId = null;
|
|
5605
|
+
}
|
|
5606
|
+
try {
|
|
5607
|
+
await this.listenContext?.close();
|
|
5608
|
+
} catch (error) {
|
|
5609
|
+
logRuntimeError("voice.stopListening", error);
|
|
5610
|
+
}
|
|
5611
|
+
this.listenContext = null;
|
|
5612
|
+
this.listenAnalyser = null;
|
|
5613
|
+
this.listenSource = null;
|
|
5614
|
+
if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
5615
|
+
this.stopRecording();
|
|
5616
|
+
}
|
|
5617
|
+
this.recordingStartedFromListening = false;
|
|
5618
|
+
this.releaseInputStreamIfIdle();
|
|
5619
|
+
}
|
|
5620
|
+
dispose() {
|
|
5621
|
+
void this.stopListening();
|
|
5622
|
+
this.stopRecording();
|
|
5623
|
+
this.clearRecordingTimeout();
|
|
5624
|
+
void this.stopPlaying();
|
|
5625
|
+
if (typeof document !== "undefined") {
|
|
5626
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
5627
|
+
}
|
|
5628
|
+
if (typeof window !== "undefined") {
|
|
5629
|
+
window.removeEventListener("pagehide", this.pageHideHandler);
|
|
5630
|
+
}
|
|
5631
|
+
this.stopStreamTracks();
|
|
5632
|
+
}
|
|
5633
|
+
startRecordingInternal(stream, fromListening) {
|
|
5634
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
5635
|
+
return;
|
|
5636
|
+
}
|
|
5637
|
+
if (typeof MediaRecorder === "undefined") {
|
|
5638
|
+
throw new Error("MediaRecorder is not supported in this browser");
|
|
5639
|
+
}
|
|
5640
|
+
const preferredMimeType = this.getPreferredRecordingMimeType();
|
|
5641
|
+
const options = preferredMimeType ? { mimeType: preferredMimeType } : void 0;
|
|
5642
|
+
const recorder = options ? new MediaRecorder(stream, options) : new MediaRecorder(stream);
|
|
5643
|
+
this.mediaRecorder = recorder;
|
|
5644
|
+
this.recordingChunks = [];
|
|
5645
|
+
this.recordingStartedFromListening = fromListening;
|
|
5646
|
+
recorder.ondataavailable = (event) => {
|
|
5647
|
+
if (event.data && event.data.size > 0) {
|
|
5648
|
+
this.recordingChunks.push(event.data);
|
|
5649
|
+
}
|
|
5650
|
+
};
|
|
5651
|
+
recorder.onerror = (event) => {
|
|
5652
|
+
void this.handleRecordingError(event.error ?? new Error("Unknown recording error"));
|
|
5653
|
+
};
|
|
5654
|
+
recorder.onstart = () => {
|
|
5655
|
+
logRuntimeDebug(this.debugLogging, "voice-recording-started", {
|
|
5656
|
+
mimeType: recorder.mimeType || preferredMimeType || "unknown",
|
|
5657
|
+
fromListening
|
|
5658
|
+
});
|
|
5659
|
+
void this.triggerSystemEvent("onRecordingStarted", null);
|
|
5660
|
+
};
|
|
5661
|
+
recorder.onstop = () => {
|
|
5662
|
+
void this.handleRecorderStop(recorder);
|
|
5663
|
+
};
|
|
5664
|
+
recorder.start();
|
|
5665
|
+
this.clearRecordingTimeout();
|
|
5666
|
+
this.recordingTimeoutId = window.setTimeout(() => {
|
|
5667
|
+
void this.stopRecording();
|
|
5668
|
+
}, MAX_RECORDING_MS);
|
|
5669
|
+
}
|
|
5670
|
+
async handleRecorderStop(recorder) {
|
|
5671
|
+
this.clearRecordingTimeout();
|
|
5672
|
+
const mimeType = recorder.mimeType || this.getPreferredRecordingMimeType() || "audio/webm";
|
|
5673
|
+
const blob = new Blob(this.recordingChunks, { type: mimeType });
|
|
5674
|
+
this.recordingChunks = [];
|
|
5675
|
+
this.mediaRecorder = null;
|
|
5676
|
+
try {
|
|
5677
|
+
const audioData = await blobToBase64(blob);
|
|
5678
|
+
logRuntimeDebug(this.debugLogging, "voice-recording-stopped", {
|
|
5679
|
+
mimeType,
|
|
5680
|
+
size: blob.size,
|
|
5681
|
+
fromListening: this.recordingStartedFromListening
|
|
5682
|
+
});
|
|
5683
|
+
await this.triggerSystemEvent("onRecordingStopped", {
|
|
5684
|
+
audioData,
|
|
5685
|
+
mimeType
|
|
5686
|
+
});
|
|
5687
|
+
} catch (error) {
|
|
5688
|
+
await this.handleRecordingError(error);
|
|
5689
|
+
} finally {
|
|
5690
|
+
this.recordingStartedFromListening = false;
|
|
5691
|
+
this.releaseInputStreamIfIdle();
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
async handleRecordingError(error) {
|
|
5695
|
+
logRuntimeError("voice.recording", error);
|
|
5696
|
+
await this.triggerSystemEvent("onRecordingError", this.normalizeErrorMessage(error));
|
|
5697
|
+
}
|
|
5698
|
+
async handleListeningError(error) {
|
|
5699
|
+
logRuntimeError("voice.listening", error);
|
|
5700
|
+
const message = this.normalizeErrorMessage(error);
|
|
5701
|
+
await this.triggerSystemEvent("onListeningError", message);
|
|
5702
|
+
await this.triggerSystemEvent("onListeringError", message);
|
|
5703
|
+
}
|
|
5704
|
+
normalizePlayablePayload(value) {
|
|
5705
|
+
if (typeof value === "string" && value.trim()) {
|
|
5706
|
+
const bytes = decodeBase64(value.trim());
|
|
5707
|
+
return {
|
|
5708
|
+
audioData: value.trim(),
|
|
5709
|
+
mimeType: detectMimeType(bytes)
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
5712
|
+
if (isPlayableAudioPayload(value) && typeof value.audioData === "string" && value.audioData.trim()) {
|
|
5713
|
+
const audioData = value.audioData.trim();
|
|
5714
|
+
const mimeType = typeof value.mimeType === "string" && value.mimeType.trim() ? value.mimeType.trim() : detectMimeType(decodeBase64(audioData));
|
|
5715
|
+
return { audioData, mimeType };
|
|
5716
|
+
}
|
|
5717
|
+
return null;
|
|
5718
|
+
}
|
|
5719
|
+
getPreferredRecordingMimeType() {
|
|
5720
|
+
if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
|
|
5721
|
+
return null;
|
|
5722
|
+
}
|
|
5723
|
+
for (const candidate of RECORDING_MIME_CANDIDATES) {
|
|
5724
|
+
if (MediaRecorder.isTypeSupported(candidate)) {
|
|
5725
|
+
return candidate;
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
return null;
|
|
5729
|
+
}
|
|
5730
|
+
async ensureInputStream() {
|
|
5731
|
+
if (this.mediaStream && this.mediaStream.active) {
|
|
5732
|
+
return this.mediaStream;
|
|
5733
|
+
}
|
|
5734
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
5735
|
+
throw new Error("Audio input is not supported in this browser");
|
|
5736
|
+
}
|
|
5737
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
5738
|
+
return this.mediaStream;
|
|
5739
|
+
}
|
|
5740
|
+
clearRecordingTimeout() {
|
|
5741
|
+
if (this.recordingTimeoutId !== null) {
|
|
5742
|
+
window.clearTimeout(this.recordingTimeoutId);
|
|
5743
|
+
this.recordingTimeoutId = null;
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
releaseInputStreamIfIdle() {
|
|
5747
|
+
if (this.listening) {
|
|
5748
|
+
return;
|
|
5749
|
+
}
|
|
5750
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
5751
|
+
return;
|
|
5752
|
+
}
|
|
5753
|
+
this.stopStreamTracks();
|
|
5754
|
+
}
|
|
5755
|
+
stopStreamTracks() {
|
|
5756
|
+
if (!this.mediaStream) {
|
|
5757
|
+
return;
|
|
5758
|
+
}
|
|
5759
|
+
for (const track of this.mediaStream.getTracks()) {
|
|
5760
|
+
track.stop();
|
|
5761
|
+
}
|
|
5762
|
+
this.mediaStream = null;
|
|
5763
|
+
}
|
|
5764
|
+
normalizeErrorMessage(error) {
|
|
5765
|
+
if (error instanceof Error) {
|
|
5766
|
+
return error.message;
|
|
5767
|
+
}
|
|
5768
|
+
return String(error);
|
|
5769
|
+
}
|
|
5770
|
+
detachPlayer(player) {
|
|
5771
|
+
const index = this.activePlayers.indexOf(player);
|
|
5772
|
+
if (index >= 0) {
|
|
5773
|
+
this.activePlayers.splice(index, 1);
|
|
5774
|
+
}
|
|
5775
|
+
player.audio.onended = null;
|
|
5776
|
+
player.audio.onerror = null;
|
|
5777
|
+
URL.revokeObjectURL(player.url);
|
|
5778
|
+
}
|
|
5779
|
+
async finishPlayback(player, reason) {
|
|
5780
|
+
if (!this.activePlayers.includes(player)) {
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
const payload = {
|
|
5784
|
+
audioData: player.payload.audioData,
|
|
5785
|
+
mimeType: player.payload.mimeType
|
|
5786
|
+
};
|
|
5787
|
+
this.detachPlayer(player);
|
|
5788
|
+
await this.triggerSystemEvent(reason === "finished" ? "onPlayFinished" : "onPlayingStopped", payload);
|
|
5789
|
+
}
|
|
5790
|
+
async handlePageDeactivation() {
|
|
5791
|
+
const hadActiveRecording = Boolean(this.mediaRecorder && this.mediaRecorder.state !== "inactive");
|
|
5792
|
+
const hadActiveListening = this.listening;
|
|
5793
|
+
if (!hadActiveRecording && !hadActiveListening) {
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
if (hadActiveRecording) {
|
|
5797
|
+
await this.handleRecordingError(new Error("Recording stopped because the browser tab became inactive"));
|
|
5798
|
+
this.stopRecording();
|
|
5799
|
+
}
|
|
5800
|
+
if (hadActiveListening) {
|
|
5801
|
+
await this.stopListening();
|
|
5802
|
+
if (!hadActiveRecording) {
|
|
5803
|
+
await this.handleListeningError(new Error("Listening stopped because the browser tab became inactive"));
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
};
|
|
5808
|
+
|
|
5132
5809
|
// src/lib/widgets/adaptive-layout.ts
|
|
5133
5810
|
var renderAdaptiveLayout = (env, node, _state, key, path, itemContext) => {
|
|
5134
5811
|
const mobile = env.isMobileViewport();
|
|
@@ -5587,7 +6264,7 @@ var renderOverlayContainer = (env, node, _state, key, path, itemContext) => {
|
|
|
5587
6264
|
var renderTabs = (env, node, state, key, path, itemContext) => {
|
|
5588
6265
|
const tabs = node.tabs ?? [];
|
|
5589
6266
|
const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
|
|
5590
|
-
const rawActiveTab = typeof
|
|
6267
|
+
const rawActiveTab = typeof state.activeTab === "number" ? state.activeTab : typeof node.activeTab === "number" ? node.activeTab : 0;
|
|
5591
6268
|
const activeTab = Math.max(0, Math.min(rawActiveTab, Math.max(tabs.length - 1, 0)));
|
|
5592
6269
|
const headers = tabs.map((tab, index) => `<button class="vjt-tab-button${index === activeTab ? " is-active" : ""}" type="button" role="tab" aria-selected="${index === activeTab ? "true" : "false"}" data-widget="tabs" data-widget-key="${env.escapeHtml(key)}" data-tab-index="${index}"${enabled ? "" : " disabled"}>${env.escapeHtml(env.resolveI18nValue(tab.name))}</button>`).join("");
|
|
5593
6270
|
const content = tabs[activeTab]?.content ? env.renderNode(tabs[activeTab].content, `${path}.tabs.${activeTab}`, itemContext) : "";
|
|
@@ -5654,6 +6331,21 @@ function renderModalOverlay(env, modalId) {
|
|
|
5654
6331
|
}
|
|
5655
6332
|
|
|
5656
6333
|
// src/lib/widgets/context-menu.ts
|
|
6334
|
+
var MENU_WIDTH = 220;
|
|
6335
|
+
var MENU_ITEM_HEIGHT = 44;
|
|
6336
|
+
var MENU_PADDING = 8;
|
|
6337
|
+
function getContextMenuItemAtPath(items, path) {
|
|
6338
|
+
let currentItems = items ?? [];
|
|
6339
|
+
let currentItem = null;
|
|
6340
|
+
for (const index of path) {
|
|
6341
|
+
currentItem = currentItems[index] ?? null;
|
|
6342
|
+
if (!currentItem) {
|
|
6343
|
+
return null;
|
|
6344
|
+
}
|
|
6345
|
+
currentItems = currentItem.items ?? [];
|
|
6346
|
+
}
|
|
6347
|
+
return currentItem;
|
|
6348
|
+
}
|
|
5657
6349
|
function renderContextMenuOverlay(env, menu) {
|
|
5658
6350
|
const node = env.nodeById.get(menu.id);
|
|
5659
6351
|
if (!node || node.widget !== "context-menu") {
|
|
@@ -5661,8 +6353,52 @@ function renderContextMenuOverlay(env, menu) {
|
|
|
5661
6353
|
}
|
|
5662
6354
|
const state = env.ensureWidgetState(node, menu.id);
|
|
5663
6355
|
const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
|
|
5664
|
-
const
|
|
5665
|
-
|
|
6356
|
+
const openPath = Array.isArray(menu.openPath) ? menu.openPath : [];
|
|
6357
|
+
const layers = [];
|
|
6358
|
+
let items = node.items ?? [];
|
|
6359
|
+
let pathPrefix = [];
|
|
6360
|
+
let layerX = menu.x;
|
|
6361
|
+
let layerY = menu.y;
|
|
6362
|
+
while (items.length > 0) {
|
|
6363
|
+
layers.push(renderMenuLayer(env, menu.id, items, enabled, layerX, layerY, pathPrefix));
|
|
6364
|
+
const nextIndex = openPath[pathPrefix.length];
|
|
6365
|
+
if (typeof nextIndex !== "number") {
|
|
6366
|
+
break;
|
|
6367
|
+
}
|
|
6368
|
+
const nextItem = items[nextIndex];
|
|
6369
|
+
if (!nextItem?.items?.length) {
|
|
6370
|
+
break;
|
|
6371
|
+
}
|
|
6372
|
+
const proposedY = layerY + MENU_PADDING + nextIndex * MENU_ITEM_HEIGHT;
|
|
6373
|
+
const rightX = layerX + MENU_WIDTH - MENU_PADDING;
|
|
6374
|
+
const leftX = layerX - MENU_WIDTH + MENU_PADDING;
|
|
6375
|
+
layerX = rightX + MENU_WIDTH <= window.innerWidth ? rightX : Math.max(8, leftX);
|
|
6376
|
+
layerY = Math.max(8, Math.min(proposedY, window.innerHeight - (16 + nextItem.items.length * MENU_ITEM_HEIGHT) - 8));
|
|
6377
|
+
items = nextItem.items;
|
|
6378
|
+
pathPrefix = [...pathPrefix, nextIndex];
|
|
6379
|
+
}
|
|
6380
|
+
return `<div class="vjt-context-menu-backdrop" data-context-menu-backdrop="${env.escapeHtml(menu.id)}">${layers.join("")}</div>`;
|
|
6381
|
+
}
|
|
6382
|
+
function renderMenuLayer(env, menuId, items, enabled, x, y, pathPrefix) {
|
|
6383
|
+
const content = items.map((item, index) => {
|
|
6384
|
+
const path = [...pathPrefix, index];
|
|
6385
|
+
const pathText = path.join(".");
|
|
6386
|
+
const label = env.escapeHtml(env.resolveI18nValue(item.name));
|
|
6387
|
+
const disabled = enabled ? "" : " disabled";
|
|
6388
|
+
if (item.items?.length) {
|
|
6389
|
+
return `<button class="vjt-context-menu-item vjt-context-menu-item--parent" type="button" data-context-menu-expand="${env.escapeHtml(menuId)}:${env.escapeHtml(pathText)}"${disabled}><span>${label}</span><span class="vjt-context-menu-chevron">></span></button>`;
|
|
6390
|
+
}
|
|
6391
|
+
return `<button class="vjt-context-menu-item" type="button" data-context-menu-item="${env.escapeHtml(menuId)}:${env.escapeHtml(pathText)}"${disabled}>${label}</button>`;
|
|
6392
|
+
}).join("");
|
|
6393
|
+
return `<div class="vjt-context-menu" style="left:${x}px;top:${y}px">${content}</div>`;
|
|
6394
|
+
}
|
|
6395
|
+
|
|
6396
|
+
// src/lib/widgets/confirm-modal.ts
|
|
6397
|
+
function renderConfirmModalOverlay(env, modal) {
|
|
6398
|
+
const caption = env.escapeHtml(env.resolveI18nValue(modal.caption));
|
|
6399
|
+
const yes = env.escapeHtml(env.resolveI18nValue(modal.yes));
|
|
6400
|
+
const no = env.escapeHtml(env.resolveI18nValue(modal.no));
|
|
6401
|
+
return `<div class="vjt-confirm-backdrop" data-confirm-backdrop="true"><div class="vjt-confirm-window"><div class="vjt-confirm-caption">${caption}</div><div class="vjt-confirm-actions"><button class="vjt-button vjt-button--bright" type="button" data-confirm-yes="true">${yes}</button><button class="vjt-button vjt-button--regular" type="button" data-confirm-no="true">${no}</button></div></div></div>`;
|
|
5666
6402
|
}
|
|
5667
6403
|
|
|
5668
6404
|
// src/lib/widgets/bindings.ts
|
|
@@ -5854,7 +6590,7 @@ function bindWidgetEvents(root, env) {
|
|
|
5854
6590
|
});
|
|
5855
6591
|
continue;
|
|
5856
6592
|
}
|
|
5857
|
-
if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && env.getInlineActions(node)?.length) {
|
|
6593
|
+
if (element instanceof HTMLAnchorElement && element.dataset.widget === "link" && (node.events?.onClick?.length || env.getInlineActions(node)?.length)) {
|
|
5858
6594
|
element.addEventListener("pointerdown", (event) => {
|
|
5859
6595
|
env.setLastPointer({ x: event.clientX, y: event.clientY });
|
|
5860
6596
|
});
|
|
@@ -5924,6 +6660,45 @@ function bindDelegatedUi(root, env) {
|
|
|
5924
6660
|
window.addEventListener("pointercancel", () => {
|
|
5925
6661
|
env.stopSplitterDrag();
|
|
5926
6662
|
});
|
|
6663
|
+
root.addEventListener("pointerover", (event) => {
|
|
6664
|
+
void (async () => {
|
|
6665
|
+
const target = event.target;
|
|
6666
|
+
if (!(target instanceof Element)) {
|
|
6667
|
+
return;
|
|
6668
|
+
}
|
|
6669
|
+
const hoveredItem = target.closest("[data-context-menu-item], [data-context-menu-expand]");
|
|
6670
|
+
if (!(hoveredItem instanceof HTMLButtonElement)) {
|
|
6671
|
+
return;
|
|
6672
|
+
}
|
|
6673
|
+
const descriptor = hoveredItem.dataset.contextMenuExpand ?? hoveredItem.dataset.contextMenuItem;
|
|
6674
|
+
if (!descriptor) {
|
|
6675
|
+
return;
|
|
6676
|
+
}
|
|
6677
|
+
const [menuId, pathText = ""] = descriptor.split(":");
|
|
6678
|
+
const node = env.nodeById.get(menuId);
|
|
6679
|
+
if (node?.widget !== "context-menu") {
|
|
6680
|
+
return;
|
|
6681
|
+
}
|
|
6682
|
+
const activeMenu = env.getActiveContextMenu();
|
|
6683
|
+
if (!activeMenu || activeMenu.id !== menuId) {
|
|
6684
|
+
return;
|
|
6685
|
+
}
|
|
6686
|
+
const path = parseMenuPath(pathText);
|
|
6687
|
+
const item = getContextMenuItemAtPath(node.items, path);
|
|
6688
|
+
if (!item) {
|
|
6689
|
+
return;
|
|
6690
|
+
}
|
|
6691
|
+
const nextOpenPath = item.items?.length ? path : path.slice(0, -1);
|
|
6692
|
+
if (samePath(activeMenu.openPath ?? [], nextOpenPath)) {
|
|
6693
|
+
return;
|
|
6694
|
+
}
|
|
6695
|
+
env.setActiveContextMenu({
|
|
6696
|
+
...activeMenu,
|
|
6697
|
+
openPath: nextOpenPath
|
|
6698
|
+
});
|
|
6699
|
+
await env.rerenderRoot();
|
|
6700
|
+
})();
|
|
6701
|
+
});
|
|
5927
6702
|
root.addEventListener("click", (event) => {
|
|
5928
6703
|
void (async () => {
|
|
5929
6704
|
const target = event.target;
|
|
@@ -5945,8 +6720,6 @@ function bindDelegatedUi(root, env) {
|
|
|
5945
6720
|
const state = env.stateByKey.get(key);
|
|
5946
6721
|
if (state) {
|
|
5947
6722
|
state.activeTab = Number.parseInt(tabButton.dataset.tabIndex ?? "0", 10) || 0;
|
|
5948
|
-
const tabsNode = node;
|
|
5949
|
-
tabsNode.activeTab = state.activeTab;
|
|
5950
6723
|
}
|
|
5951
6724
|
await env.rerenderRoot();
|
|
5952
6725
|
return;
|
|
@@ -6004,7 +6777,7 @@ function bindDelegatedUi(root, env) {
|
|
|
6004
6777
|
}
|
|
6005
6778
|
const menuItem = target.closest("[data-context-menu-item]");
|
|
6006
6779
|
if (menuItem instanceof HTMLButtonElement) {
|
|
6007
|
-
const [menuId,
|
|
6780
|
+
const [menuId, pathText = ""] = (menuItem.dataset.contextMenuItem ?? "").split(":");
|
|
6008
6781
|
const node = env.nodeById.get(menuId);
|
|
6009
6782
|
if (node?.widget !== "context-menu") {
|
|
6010
6783
|
return;
|
|
@@ -6012,14 +6785,30 @@ function bindDelegatedUi(root, env) {
|
|
|
6012
6785
|
if (!env.isWidgetEnabled(node, menuId)) {
|
|
6013
6786
|
return;
|
|
6014
6787
|
}
|
|
6015
|
-
const item = node.items
|
|
6788
|
+
const item = getContextMenuItemAtPath(node.items, parseMenuPath(pathText));
|
|
6789
|
+
const menuContext = env.getActiveContextMenu();
|
|
6790
|
+
const currentValue = menuContext?.id === menuId ? menuContext.currentValue ?? null : null;
|
|
6016
6791
|
env.setActiveContextMenu(null);
|
|
6017
6792
|
if (item?.actions?.length) {
|
|
6018
|
-
await env.runActions(item.actions,
|
|
6793
|
+
await env.runActions(item.actions, currentValue, { currentValue });
|
|
6019
6794
|
}
|
|
6020
6795
|
await env.rerenderRoot();
|
|
6021
6796
|
return;
|
|
6022
6797
|
}
|
|
6798
|
+
const menuParentItem = target.closest("[data-context-menu-expand]");
|
|
6799
|
+
if (menuParentItem instanceof HTMLButtonElement) {
|
|
6800
|
+
const [menuId, pathText = ""] = (menuParentItem.dataset.contextMenuExpand ?? "").split(":");
|
|
6801
|
+
const activeMenu = env.getActiveContextMenu();
|
|
6802
|
+
if (!activeMenu || activeMenu.id !== menuId) {
|
|
6803
|
+
return;
|
|
6804
|
+
}
|
|
6805
|
+
env.setActiveContextMenu({
|
|
6806
|
+
...activeMenu,
|
|
6807
|
+
openPath: parseMenuPath(pathText)
|
|
6808
|
+
});
|
|
6809
|
+
await env.rerenderRoot();
|
|
6810
|
+
return;
|
|
6811
|
+
}
|
|
6023
6812
|
const modalButton = target.closest("[data-modal-button]");
|
|
6024
6813
|
if (modalButton instanceof HTMLButtonElement) {
|
|
6025
6814
|
const [modalId, indexValue] = (modalButton.dataset.modalButton ?? "").split(":");
|
|
@@ -6035,10 +6824,32 @@ function bindDelegatedUi(root, env) {
|
|
|
6035
6824
|
await env.runActions(item.actions, null, { currentValue: null });
|
|
6036
6825
|
}
|
|
6037
6826
|
await env.rerenderRoot();
|
|
6827
|
+
return;
|
|
6828
|
+
}
|
|
6829
|
+
if (target.closest("[data-confirm-yes]")) {
|
|
6830
|
+
env.resolveConfirmModal(true);
|
|
6831
|
+
await env.rerenderRoot();
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
if (target.closest("[data-confirm-no]")) {
|
|
6835
|
+
env.resolveConfirmModal(false);
|
|
6836
|
+
await env.rerenderRoot();
|
|
6038
6837
|
}
|
|
6039
6838
|
})();
|
|
6040
6839
|
});
|
|
6041
6840
|
}
|
|
6841
|
+
function parseMenuPath(value) {
|
|
6842
|
+
if (!value) {
|
|
6843
|
+
return [];
|
|
6844
|
+
}
|
|
6845
|
+
return value.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => !Number.isNaN(part));
|
|
6846
|
+
}
|
|
6847
|
+
function samePath(left, right) {
|
|
6848
|
+
if (left.length !== right.length) {
|
|
6849
|
+
return false;
|
|
6850
|
+
}
|
|
6851
|
+
return left.every((value, index) => value === right[index]);
|
|
6852
|
+
}
|
|
6042
6853
|
|
|
6043
6854
|
// src/lib/render.ts
|
|
6044
6855
|
var DEFAULT_DATE_FORMAT = "dd.MM.yyyy";
|
|
@@ -6183,16 +6994,19 @@ var RuntimeRenderer = class {
|
|
|
6183
6994
|
stateByKey = /* @__PURE__ */ new Map();
|
|
6184
6995
|
nodeById = /* @__PURE__ */ new Map();
|
|
6185
6996
|
stateById = /* @__PURE__ */ new Map();
|
|
6997
|
+
vars = /* @__PURE__ */ new Map();
|
|
6186
6998
|
resizeHandler = null;
|
|
6187
6999
|
pointerHandler = null;
|
|
6188
7000
|
eventSources = [];
|
|
6189
7001
|
referenceRuntime;
|
|
6190
7002
|
networkRuntime;
|
|
6191
7003
|
actionRuntime;
|
|
7004
|
+
voiceRuntime;
|
|
6192
7005
|
splitterDragState = null;
|
|
6193
7006
|
hasTriggeredInitialRefresh = false;
|
|
6194
7007
|
activeModalId = null;
|
|
6195
7008
|
activeContextMenu = null;
|
|
7009
|
+
activeConfirmModal = null;
|
|
6196
7010
|
lastPointer = { x: 24, y: 24 };
|
|
6197
7011
|
pendingResetModalIds = /* @__PURE__ */ new Set();
|
|
6198
7012
|
constructor(description, options = {}) {
|
|
@@ -6255,6 +7069,10 @@ var RuntimeRenderer = class {
|
|
|
6255
7069
|
break;
|
|
6256
7070
|
}
|
|
6257
7071
|
},
|
|
7072
|
+
getVarValue: (name) => this.vars.get(name),
|
|
7073
|
+
setVarValue: (name, value) => {
|
|
7074
|
+
this.vars.set(name, value);
|
|
7075
|
+
},
|
|
6258
7076
|
ensureWidgetState: (node, key) => this.ensureWidgetState(node, key),
|
|
6259
7077
|
resolveItemNodeKey: (node, listKey, index, fallbackPath) => this.resolveItemNodeKey(node, listKey, index, fallbackPath),
|
|
6260
7078
|
indexListElementNodes: (listNode, element, index) => this.indexListElementNodes(listNode, element, index)
|
|
@@ -6268,12 +7086,17 @@ var RuntimeRenderer = class {
|
|
|
6268
7086
|
runActions: (actions, inputValue, context) => this.actionRuntime.runActions(actions, inputValue, context),
|
|
6269
7087
|
rerenderRoot: () => this.rerenderRoot()
|
|
6270
7088
|
});
|
|
7089
|
+
this.voiceRuntime = new VoiceRuntime({
|
|
7090
|
+
debugLogging: this.debugLogging,
|
|
7091
|
+
triggerSystemEvent: (eventName, inputValue) => this.triggerRuntimeSystemEvent(eventName, inputValue)
|
|
7092
|
+
});
|
|
6271
7093
|
this.actionRuntime = new ActionRuntime({
|
|
6272
7094
|
debugLogging: this.debugLogging,
|
|
6273
7095
|
actionsMap: this.actionsMap,
|
|
6274
7096
|
actionFunctions: this.actionFunctions,
|
|
6275
7097
|
nodeById: this.nodeById,
|
|
6276
7098
|
stateById: this.stateById,
|
|
7099
|
+
stateByKey: this.stateByKey,
|
|
6277
7100
|
rerenderRoot: () => this.rerenderRoot(),
|
|
6278
7101
|
dispatchWidgetEvent: (node, eventName, key, inputValue, pointer) => this.actionRuntime.dispatchWidgetEvent(node, eventName, key, inputValue, pointer),
|
|
6279
7102
|
executeRequest: (requestName, currentValue) => this.networkRuntime.executeRequest(requestName, currentValue),
|
|
@@ -6283,6 +7106,17 @@ var RuntimeRenderer = class {
|
|
|
6283
7106
|
resolveMappedValue: (template, currentValue, responseValue) => this.referenceRuntime.resolveMappedValue(template, currentValue, responseValue),
|
|
6284
7107
|
setWidgetEnabled: (widgetId, enabled) => this.setWidgetEnabled(widgetId, enabled),
|
|
6285
7108
|
clearWidget: (widgetId) => this.clearWidget(widgetId),
|
|
7109
|
+
clearListElementState: (listKey) => this.clearListElementState(listKey),
|
|
7110
|
+
focusWidget: (reference) => this.focusWidget(reference),
|
|
7111
|
+
playAudio: (value) => this.voiceRuntime.play(value),
|
|
7112
|
+
stopPlaying: () => this.voiceRuntime.stopPlaying(),
|
|
7113
|
+
copyToClipboard: (value) => this.copyToClipboard(value),
|
|
7114
|
+
selectFile: (args) => this.selectFile(args),
|
|
7115
|
+
confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
|
|
7116
|
+
startRecording: () => this.voiceRuntime.startRecording(),
|
|
7117
|
+
stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
|
|
7118
|
+
startListening: () => this.voiceRuntime.startListening(),
|
|
7119
|
+
stopListening: () => this.voiceRuntime.stopListening(),
|
|
6286
7120
|
setUiReference: (widgetId, resourceRef) => {
|
|
6287
7121
|
const node = this.nodeById.get(widgetId);
|
|
6288
7122
|
if (!node || node.widget !== "ui-reference") {
|
|
@@ -6376,6 +7210,7 @@ var RuntimeRenderer = class {
|
|
|
6376
7210
|
source.close();
|
|
6377
7211
|
}
|
|
6378
7212
|
this.eventSources.length = 0;
|
|
7213
|
+
this.voiceRuntime.dispose();
|
|
6379
7214
|
};
|
|
6380
7215
|
}
|
|
6381
7216
|
rerenderRoot() {
|
|
@@ -6383,7 +7218,7 @@ var RuntimeRenderer = class {
|
|
|
6383
7218
|
return Promise.resolve();
|
|
6384
7219
|
}
|
|
6385
7220
|
this.reindexStaticTree();
|
|
6386
|
-
const preservedState = captureElementState(this.root, this.pendingResetModalIds
|
|
7221
|
+
const preservedState = captureElementState(this.root, this.pendingResetModalIds);
|
|
6387
7222
|
this.root.innerHTML = this.renderMarkup();
|
|
6388
7223
|
this.root.classList.toggle("vjt-root--mobile", isMobileViewport());
|
|
6389
7224
|
this.root.classList.toggle("vjt-root--desktop", !isMobileViewport());
|
|
@@ -6392,7 +7227,7 @@ var RuntimeRenderer = class {
|
|
|
6392
7227
|
}
|
|
6393
7228
|
this.attachInputBehavior(this.root);
|
|
6394
7229
|
this.attachWidgetEvents(this.root);
|
|
6395
|
-
restoreElementState(this.root, preservedState);
|
|
7230
|
+
restoreElementState(this.root, preservedState, this.stateByKey);
|
|
6396
7231
|
this.activeContextMenu = adjustContextMenuPosition(this.root, this.activeContextMenu);
|
|
6397
7232
|
this.pendingResetModalIds.clear();
|
|
6398
7233
|
if (typeof this.onRuntimeSnapshot === "function") {
|
|
@@ -6407,6 +7242,17 @@ var RuntimeRenderer = class {
|
|
|
6407
7242
|
}
|
|
6408
7243
|
await this.actionRuntime.runActions(actions, null, { currentValue: null });
|
|
6409
7244
|
}
|
|
7245
|
+
async triggerRuntimeSystemEvent(eventName, inputValue) {
|
|
7246
|
+
const actions = this.systemEvents[eventName];
|
|
7247
|
+
if (!actions?.length) {
|
|
7248
|
+
return;
|
|
7249
|
+
}
|
|
7250
|
+
await this.actionRuntime.runActions(actions, inputValue, {
|
|
7251
|
+
currentValue: inputValue,
|
|
7252
|
+
responseValue: inputValue
|
|
7253
|
+
});
|
|
7254
|
+
await this.rerenderRoot();
|
|
7255
|
+
}
|
|
6410
7256
|
async navigateTo(url) {
|
|
6411
7257
|
if (typeof window === "undefined" || !url.trim()) {
|
|
6412
7258
|
return;
|
|
@@ -6425,6 +7271,138 @@ var RuntimeRenderer = class {
|
|
|
6425
7271
|
await this.rerenderRoot();
|
|
6426
7272
|
await this.runSystemEvent("onAfterNavigate");
|
|
6427
7273
|
}
|
|
7274
|
+
async copyToClipboard(value) {
|
|
7275
|
+
const text = value == null ? "" : typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
7276
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
7277
|
+
await navigator.clipboard.writeText(text);
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
if (typeof document === "undefined") {
|
|
7281
|
+
throw new Error("Clipboard API is unavailable");
|
|
7282
|
+
}
|
|
7283
|
+
const textarea = document.createElement("textarea");
|
|
7284
|
+
textarea.value = text;
|
|
7285
|
+
textarea.setAttribute("readonly", "true");
|
|
7286
|
+
textarea.style.position = "fixed";
|
|
7287
|
+
textarea.style.left = "-9999px";
|
|
7288
|
+
textarea.style.top = "0";
|
|
7289
|
+
document.body.append(textarea);
|
|
7290
|
+
textarea.focus();
|
|
7291
|
+
textarea.select();
|
|
7292
|
+
const copied = document.execCommand("copy");
|
|
7293
|
+
textarea.remove();
|
|
7294
|
+
if (!copied) {
|
|
7295
|
+
throw new Error("Failed to copy value to clipboard");
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
async selectFile(args) {
|
|
7299
|
+
if (typeof document === "undefined") {
|
|
7300
|
+
throw new Error("File selection is unavailable");
|
|
7301
|
+
}
|
|
7302
|
+
const options = isPlainObject4(args) ? args : {};
|
|
7303
|
+
const input = document.createElement("input");
|
|
7304
|
+
input.type = "file";
|
|
7305
|
+
input.style.position = "fixed";
|
|
7306
|
+
input.style.left = "-9999px";
|
|
7307
|
+
input.style.top = "0";
|
|
7308
|
+
if (typeof options.accept === "string" && options.accept.trim()) {
|
|
7309
|
+
input.accept = options.accept;
|
|
7310
|
+
}
|
|
7311
|
+
input.multiple = options.multiple === true;
|
|
7312
|
+
const selection = new Promise((resolve, reject) => {
|
|
7313
|
+
let settled = false;
|
|
7314
|
+
const finish = (value) => {
|
|
7315
|
+
if (settled) {
|
|
7316
|
+
return;
|
|
7317
|
+
}
|
|
7318
|
+
settled = true;
|
|
7319
|
+
resolve(value);
|
|
7320
|
+
};
|
|
7321
|
+
const fail = (error) => {
|
|
7322
|
+
if (settled) {
|
|
7323
|
+
return;
|
|
7324
|
+
}
|
|
7325
|
+
settled = true;
|
|
7326
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
7327
|
+
};
|
|
7328
|
+
const cleanup = () => {
|
|
7329
|
+
window.removeEventListener("focus", handleFocus, true);
|
|
7330
|
+
input.remove();
|
|
7331
|
+
};
|
|
7332
|
+
const handleFocus = () => {
|
|
7333
|
+
window.setTimeout(() => {
|
|
7334
|
+
if (!settled && (!input.files || input.files.length === 0)) {
|
|
7335
|
+
cleanup();
|
|
7336
|
+
finish(null);
|
|
7337
|
+
}
|
|
7338
|
+
}, 250);
|
|
7339
|
+
};
|
|
7340
|
+
input.addEventListener("change", () => {
|
|
7341
|
+
void (async () => {
|
|
7342
|
+
try {
|
|
7343
|
+
const files = Array.from(input.files ?? []);
|
|
7344
|
+
const converted = await Promise.all(files.map((file) => this.readSelectedFile(file)));
|
|
7345
|
+
cleanup();
|
|
7346
|
+
finish(input.multiple ? converted : converted[0] ?? null);
|
|
7347
|
+
} catch (error) {
|
|
7348
|
+
cleanup();
|
|
7349
|
+
fail(error);
|
|
7350
|
+
}
|
|
7351
|
+
})();
|
|
7352
|
+
}, { once: true });
|
|
7353
|
+
input.addEventListener("cancel", () => {
|
|
7354
|
+
cleanup();
|
|
7355
|
+
finish(null);
|
|
7356
|
+
}, { once: true });
|
|
7357
|
+
window.addEventListener("focus", handleFocus, true);
|
|
7358
|
+
document.body.append(input);
|
|
7359
|
+
input.click();
|
|
7360
|
+
});
|
|
7361
|
+
return selection;
|
|
7362
|
+
}
|
|
7363
|
+
readSelectedFile(file) {
|
|
7364
|
+
return new Promise((resolve, reject) => {
|
|
7365
|
+
const reader = new FileReader();
|
|
7366
|
+
reader.onerror = () => {
|
|
7367
|
+
reject(reader.error ?? new Error(`Failed to read file ${file.name}`));
|
|
7368
|
+
};
|
|
7369
|
+
reader.onload = () => {
|
|
7370
|
+
const result = typeof reader.result === "string" ? reader.result : "";
|
|
7371
|
+
const [, data = ""] = result.split(",", 2);
|
|
7372
|
+
resolve({
|
|
7373
|
+
name: file.name,
|
|
7374
|
+
mimeType: file.type || "application/octet-stream",
|
|
7375
|
+
size: file.size,
|
|
7376
|
+
data
|
|
7377
|
+
});
|
|
7378
|
+
};
|
|
7379
|
+
reader.readAsDataURL(file);
|
|
7380
|
+
});
|
|
7381
|
+
}
|
|
7382
|
+
async confirmModal(args, _inputValue) {
|
|
7383
|
+
const options = isPlainObject4(args) ? args : {};
|
|
7384
|
+
const caption = typeof options.caption === "string" && options.caption ? options.caption : "Confirm?";
|
|
7385
|
+
const yes = typeof options.yes === "string" && options.yes ? options.yes : "Yes";
|
|
7386
|
+
const no = typeof options.no === "string" && options.no ? options.no : "No";
|
|
7387
|
+
return new Promise((resolve) => {
|
|
7388
|
+
this.activeConfirmModal?.resolve(false);
|
|
7389
|
+
this.activeConfirmModal = {
|
|
7390
|
+
caption,
|
|
7391
|
+
yes,
|
|
7392
|
+
no,
|
|
7393
|
+
resolve
|
|
7394
|
+
};
|
|
7395
|
+
void this.rerenderRoot();
|
|
7396
|
+
});
|
|
7397
|
+
}
|
|
7398
|
+
resolveConfirmModal(confirmed) {
|
|
7399
|
+
const activeModal = this.activeConfirmModal;
|
|
7400
|
+
if (!activeModal) {
|
|
7401
|
+
return;
|
|
7402
|
+
}
|
|
7403
|
+
this.activeConfirmModal = null;
|
|
7404
|
+
activeModal.resolve(confirmed);
|
|
7405
|
+
}
|
|
6428
7406
|
reindexStaticTree() {
|
|
6429
7407
|
this.nodeByKey.clear();
|
|
6430
7408
|
this.nodeById.clear();
|
|
@@ -6461,6 +7439,12 @@ var RuntimeRenderer = class {
|
|
|
6461
7439
|
this.stateById.set(restoredState.id, restoredState);
|
|
6462
7440
|
}
|
|
6463
7441
|
}
|
|
7442
|
+
this.vars.clear();
|
|
7443
|
+
for (const [name, value] of snapshot.vars ?? []) {
|
|
7444
|
+
if (typeof name === "string" && name.length > 0) {
|
|
7445
|
+
this.vars.set(name, deepClone(value));
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
6464
7448
|
if (snapshot.activeModalId && this.nodeById.get(snapshot.activeModalId)?.widget === "modal-window") {
|
|
6465
7449
|
this.activeModalId = snapshot.activeModalId;
|
|
6466
7450
|
}
|
|
@@ -6471,8 +7455,13 @@ var RuntimeRenderer = class {
|
|
|
6471
7455
|
createRuntimeSnapshot() {
|
|
6472
7456
|
return {
|
|
6473
7457
|
stateByKey: Array.from(this.stateByKey.entries(), ([key, state]) => [key, deepClone(state)]),
|
|
7458
|
+
vars: Array.from(this.vars.entries(), ([name, value]) => [name, deepClone(value)]),
|
|
6474
7459
|
activeModalId: this.activeModalId,
|
|
6475
|
-
activeContextMenu: this.activeContextMenu ?
|
|
7460
|
+
activeContextMenu: this.activeContextMenu ? {
|
|
7461
|
+
id: this.activeContextMenu.id,
|
|
7462
|
+
x: this.activeContextMenu.x,
|
|
7463
|
+
y: this.activeContextMenu.y
|
|
7464
|
+
} : null
|
|
6476
7465
|
};
|
|
6477
7466
|
}
|
|
6478
7467
|
indexStaticNodes(node, path) {
|
|
@@ -6982,7 +7971,8 @@ var RuntimeRenderer = class {
|
|
|
6982
7971
|
const env = this.getWidgetRenderEnvironment();
|
|
6983
7972
|
const modalMarkup = this.activeModalId ? renderModalOverlay(env, this.activeModalId) : "";
|
|
6984
7973
|
const menuMarkup = this.activeContextMenu ? renderContextMenuOverlay(env, this.activeContextMenu) : "";
|
|
6985
|
-
|
|
7974
|
+
const confirmMarkup = this.activeConfirmModal ? renderConfirmModalOverlay(env, this.activeConfirmModal) : "";
|
|
7975
|
+
return `${modalMarkup}${menuMarkup}${confirmMarkup}`;
|
|
6986
7976
|
}
|
|
6987
7977
|
updatePointerFromEvent(event) {
|
|
6988
7978
|
this.lastPointer = updatePointerFromMouseEvent(event);
|
|
@@ -7209,6 +8199,7 @@ var RuntimeRenderer = class {
|
|
|
7209
8199
|
nodeByKey: this.nodeByKey,
|
|
7210
8200
|
stateByKey: this.stateByKey,
|
|
7211
8201
|
getLastPointer: () => this.lastPointer,
|
|
8202
|
+
getActiveContextMenu: () => this.activeContextMenu,
|
|
7212
8203
|
setActiveContextMenu: (menu) => {
|
|
7213
8204
|
this.activeContextMenu = menu;
|
|
7214
8205
|
},
|
|
@@ -7218,6 +8209,7 @@ var RuntimeRenderer = class {
|
|
|
7218
8209
|
updateSplitterDrag: (clientPosition) => this.updateSplitterDrag(clientPosition),
|
|
7219
8210
|
stopSplitterDrag: () => this.stopSplitterDrag(),
|
|
7220
8211
|
hasSplitterDrag: () => this.splitterDragState !== null,
|
|
8212
|
+
resolveConfirmModal: (confirmed) => this.resolveConfirmModal(confirmed),
|
|
7221
8213
|
closeModal: (modalId) => this.closeModal(modalId),
|
|
7222
8214
|
rerenderRoot: () => this.rerenderRoot(),
|
|
7223
8215
|
redispatchUnderlyingClick: (x, y) => this.redispatchUnderlyingClick(x, y),
|
|
@@ -7414,6 +8406,28 @@ var RuntimeRenderer = class {
|
|
|
7414
8406
|
clearListElementState(listKey) {
|
|
7415
8407
|
this.referenceRuntime.clearListElementState(listKey);
|
|
7416
8408
|
}
|
|
8409
|
+
focusWidget(reference) {
|
|
8410
|
+
if (!(this.root instanceof HTMLElement)) {
|
|
8411
|
+
return;
|
|
8412
|
+
}
|
|
8413
|
+
const widgetId = reference.split(".")[0]?.trim();
|
|
8414
|
+
if (!widgetId) {
|
|
8415
|
+
return;
|
|
8416
|
+
}
|
|
8417
|
+
const directTarget = this.root.querySelector(`#${CSS.escape(widgetId)}`);
|
|
8418
|
+
if (directTarget && typeof directTarget.focus === "function") {
|
|
8419
|
+
directTarget.focus({ preventScroll: true });
|
|
8420
|
+
return;
|
|
8421
|
+
}
|
|
8422
|
+
const widgetRoot = this.root.querySelector(`[data-widget-id="${CSS.escape(widgetId)}"]`);
|
|
8423
|
+
if (!widgetRoot) {
|
|
8424
|
+
return;
|
|
8425
|
+
}
|
|
8426
|
+
const nestedTarget = widgetRoot.matches("input, textarea, select, button, a[href], [tabindex]") ? widgetRoot : widgetRoot.querySelector("input, textarea, select, button, a[href], [tabindex]");
|
|
8427
|
+
if (nestedTarget && typeof nestedTarget.focus === "function") {
|
|
8428
|
+
nestedTarget.focus({ preventScroll: true });
|
|
8429
|
+
}
|
|
8430
|
+
}
|
|
7417
8431
|
};
|
|
7418
8432
|
function renderJson(description, options = {}) {
|
|
7419
8433
|
const renderer = new RuntimeRenderer(description, options);
|