@vortexm/vjt 0.1.4 → 0.1.5

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 CHANGED
@@ -1,4 +1,4 @@
1
1
  export { renderApp, renderJson } from './lib/render.js';
2
2
  export { ResourceManager } from './lib/resource-manager.js';
3
3
  export { DEFAULT_STYLE_MAP } from './lib/default-styles.js';
4
- export type { ActionDefinition, ActionMap, AdaptiveLayoutNode, BaseNode, ButtonNode, CheckboxNode, ComboboxNode, ConditionExpression, ConditionalContainerNode, ContextMenuNode, ContainerCell, ContainerLayoutNode, CellConstraints, DescriptionNode, GridViewNode, HeadingTag, I18nMap, ImageNode, LayoutType, LinkNode, ListElementContext, ListNode, ListboxNode, MdTextNode, ModalWindowNode, MountJsonOptions, OverlayContainerNode, OverlayLayer, PanelNode, PrimitiveRequestType, RadioGroupNode, RequestDefinition, RequestMap, RequestSchema, RenderJsonOptions, RuntimeSnapshot, SseConfig, SseConfigInput, SseEventDefinition, SpoilerNode, SplitterNode, StaticTextNode, StyleMap, TabsNode, TextAlign, TextareaNode, UiReferenceNode, Theme, VerticalAlign, WidgetEventName, WidgetState } from './lib/types.js';
4
+ export type { ActionDefinition, ActionMap, AdaptiveLayoutNode, BaseNode, ButtonNode, CheckboxNode, ComboboxNode, ConditionExpression, ConditionalContainerNode, ContextMenuNode, ContainerCell, ContainerLayoutNode, CellConstraints, DescriptionNode, GridViewNode, HeadingTag, I18nMap, ImageNode, LayoutType, LinkNode, ListElementContext, ListNode, ListboxNode, MdTextNode, ModalWindowNode, MountJsonOptions, OverlayContainerNode, OverlayLayer, PanelNode, PrimitiveRequestType, RadioGroupNode, RequestDefinition, RequestMap, RequestSchema, RenderJsonOptions, RuntimeSnapshot, SseConfig, SseConfigInput, SseEventDefinition, SpoilerNode, SplitterNode, StaticTextNode, StyleMap, SystemEventName, TabsNode, TextAlign, TextareaNode, UiReferenceNode, Theme, VerticalAlign, WidgetEventName, WidgetState } from './lib/types.js';
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
- json = JSON.parse(responseText);
3946
- } catch {
3947
- throw new Error(`Request ${requestName} returned non-JSON response: ${responseText.slice(0, 120)}`);
3948
- }
3949
- if (json.error) {
3950
- throw new Error(`Request ${requestName} returned JSON-RPC error: ${this.stringifyPrimitive(json.error.message ?? "unknown error")}`);
3951
- }
3952
- if (!definition.response) {
3953
- throw new Error(`Request ${requestName} must define response schema`);
3954
- }
3955
- const result = sanitizeSchemaValue(definition.response, json.result, `response.${requestName}`);
3956
- logRuntimeDebug(this.debugLogging, "response", {
3957
- requestName,
3958
- url: safeRequestUrl,
3959
- status: response.status,
3960
- json,
3961
- result
3962
- });
3963
- if (definition.onResponse?.length) {
3964
- await this.runActions(definition.onResponse, null, {
3965
- currentValue: null,
3966
- responseValue: result
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
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
3967
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,14 @@ var ActionRuntime = class {
4498
4578
  resolveMappedValue;
4499
4579
  setWidgetEnabled;
4500
4580
  clearWidget;
4581
+ clearListElementState;
4582
+ focusWidget;
4583
+ playAudio;
4584
+ stopPlaying;
4585
+ startRecording;
4586
+ stopRecording;
4587
+ startListening;
4588
+ stopListening;
4501
4589
  setUiReference;
4502
4590
  refreshWidgetTree;
4503
4591
  renderWidgetTree;
@@ -4514,6 +4602,7 @@ var ActionRuntime = class {
4514
4602
  this.actionFunctions = options.actionFunctions;
4515
4603
  this.nodeById = options.nodeById;
4516
4604
  this.stateById = options.stateById;
4605
+ this.stateByKey = options.stateByKey;
4517
4606
  this.rerenderRoot = options.rerenderRoot;
4518
4607
  this.dispatchWidgetEventImpl = options.dispatchWidgetEvent;
4519
4608
  this.executeRequest = options.executeRequest;
@@ -4523,6 +4612,14 @@ var ActionRuntime = class {
4523
4612
  this.resolveMappedValue = options.resolveMappedValue;
4524
4613
  this.setWidgetEnabled = options.setWidgetEnabled;
4525
4614
  this.clearWidget = options.clearWidget;
4615
+ this.clearListElementState = options.clearListElementState;
4616
+ this.focusWidget = options.focusWidget;
4617
+ this.playAudio = options.playAudio;
4618
+ this.stopPlaying = options.stopPlaying;
4619
+ this.startRecording = options.startRecording;
4620
+ this.stopRecording = options.stopRecording;
4621
+ this.startListening = options.startListening;
4622
+ this.stopListening = options.stopListening;
4526
4623
  this.setUiReference = options.setUiReference;
4527
4624
  this.refreshWidgetTree = options.refreshWidgetTree;
4528
4625
  this.renderWidgetTree = options.renderWidgetTree;
@@ -4571,13 +4668,26 @@ var ActionRuntime = class {
4571
4668
  if (action.andThen?.length) {
4572
4669
  if (Array.isArray(output)) {
4573
4670
  const collected = [];
4574
- for (const entry of output) {
4671
+ for (const [index, entry] of output.entries()) {
4575
4672
  if (entry === null || entry === void 0) {
4576
4673
  continue;
4577
4674
  }
4578
- const nestedOutput = await this.runActions(action.andThen, entry, { ...context, currentValue: entry });
4675
+ const nestedOutput = await this.runActions(action.andThen, entry, {
4676
+ ...context,
4677
+ currentValue: entry,
4678
+ currentList: output,
4679
+ currentIndex: index
4680
+ });
4579
4681
  if (nestedOutput !== null && nestedOutput !== void 0) {
4580
- collected.push(nestedOutput);
4682
+ if (Array.isArray(nestedOutput)) {
4683
+ for (const item of nestedOutput) {
4684
+ if (item !== null && item !== void 0) {
4685
+ collected.push(item);
4686
+ }
4687
+ }
4688
+ } else {
4689
+ collected.push(nestedOutput);
4690
+ }
4581
4691
  }
4582
4692
  }
4583
4693
  output = collected;
@@ -4641,6 +4751,30 @@ var ActionRuntime = class {
4641
4751
  await this.navigateTo(name.slice(9));
4642
4752
  return null;
4643
4753
  }
4754
+ if (name.startsWith("play ")) {
4755
+ await this.playAudio(this.resolveReference(name.slice(5), context.currentValue, context.responseValue));
4756
+ return null;
4757
+ }
4758
+ if (name === "stopPlaying") {
4759
+ await this.stopPlaying();
4760
+ return null;
4761
+ }
4762
+ if (name === "startRecording") {
4763
+ await this.startRecording();
4764
+ return null;
4765
+ }
4766
+ if (name === "stopRecording") {
4767
+ await this.stopRecording();
4768
+ return null;
4769
+ }
4770
+ if (name === "startListening") {
4771
+ await this.startListening();
4772
+ return null;
4773
+ }
4774
+ if (name === "stopListening") {
4775
+ await this.stopListening();
4776
+ return null;
4777
+ }
4644
4778
  if (name.startsWith("showMenu ")) {
4645
4779
  const menuId = name.slice(9);
4646
4780
  const menuState = this.stateById.get(menuId);
@@ -4680,6 +4814,10 @@ var ActionRuntime = class {
4680
4814
  this.clearWidget(name.slice(6));
4681
4815
  return null;
4682
4816
  }
4817
+ if (name.startsWith("setFocus ")) {
4818
+ this.focusWidget(name.slice(9));
4819
+ return null;
4820
+ }
4683
4821
  if (name.startsWith("setUi ")) {
4684
4822
  const widgetId = name.slice(6);
4685
4823
  const resourceRef = typeof inputValue === "string" ? inputValue : "";
@@ -4702,10 +4840,74 @@ var ActionRuntime = class {
4702
4840
  this.assignReference(name.slice(4), inputValue, context.currentValue, args);
4703
4841
  return inputValue;
4704
4842
  }
4843
+ if (name.startsWith("append ")) {
4844
+ const reference = name.slice(7);
4845
+ const currentText = this.resolveReference(reference, context.currentValue, context.responseValue);
4846
+ const nextValue = `${this.stringifyValue(currentText)}${this.stringifyValue(action.args)}`;
4847
+ this.assignReference(reference, nextValue, context.currentValue);
4848
+ return nextValue;
4849
+ }
4705
4850
  if (name.startsWith("filter ")) {
4706
4851
  const value = this.resolveReference(name.slice(7), context.currentValue, context.responseValue);
4707
4852
  return value ? inputValue : null;
4708
4853
  }
4854
+ if (name === "remove") {
4855
+ const listState = this.getCurrentListState(context.currentValue);
4856
+ if (listState && typeof context.currentValue === "object" && context.currentValue !== null && "index" in context.currentValue) {
4857
+ const index = context.currentValue.index;
4858
+ if (typeof index === "number" && Array.isArray(listState.elements)) {
4859
+ listState.elements = listState.elements.filter((_, elementIndex) => elementIndex !== index);
4860
+ this.clearListElementState(listState.key);
4861
+ return listState.elements;
4862
+ }
4863
+ }
4864
+ return null;
4865
+ }
4866
+ if (name.startsWith("add ")) {
4867
+ const valueToAdd = this.cloneValue(this.unwrapListValue(this.resolveReference(name.slice(4), context.currentValue, context.responseValue)));
4868
+ const listState = this.getCurrentListState(context.currentValue);
4869
+ if (listState && Array.isArray(listState.elements)) {
4870
+ listState.elements.push(valueToAdd);
4871
+ this.clearListElementState(listState.key);
4872
+ return listState.elements;
4873
+ }
4874
+ if (context.currentList && typeof context.currentIndex === "number") {
4875
+ return context.currentIndex === context.currentList.length - 1 ? [this.unwrapListValue(inputValue), valueToAdd] : this.unwrapListValue(inputValue);
4876
+ }
4877
+ if (Array.isArray(inputValue)) {
4878
+ const nextList = [];
4879
+ for (const item of inputValue) {
4880
+ nextList.push(item);
4881
+ }
4882
+ nextList.push(valueToAdd);
4883
+ return nextList;
4884
+ }
4885
+ return inputValue;
4886
+ }
4887
+ if (name.startsWith("insert ")) {
4888
+ const valueToInsert = this.cloneValue(this.unwrapListValue(this.resolveReference(name.slice(7), context.currentValue, context.responseValue)));
4889
+ const listState = this.getCurrentListState(context.currentValue);
4890
+ if (listState && Array.isArray(listState.elements) && typeof context.currentValue === "object" && context.currentValue !== null && "index" in context.currentValue) {
4891
+ const index = context.currentValue.index;
4892
+ if (typeof index === "number") {
4893
+ listState.elements.splice(index + 1, 0, valueToInsert);
4894
+ this.clearListElementState(listState.key);
4895
+ return listState.elements;
4896
+ }
4897
+ }
4898
+ if (context.currentList && typeof context.currentIndex === "number") {
4899
+ return [this.unwrapListValue(inputValue), valueToInsert];
4900
+ }
4901
+ if (Array.isArray(inputValue)) {
4902
+ const nextList = [];
4903
+ for (const item of inputValue) {
4904
+ nextList.push(item);
4905
+ }
4906
+ nextList.push(valueToInsert);
4907
+ return nextList;
4908
+ }
4909
+ return inputValue;
4910
+ }
4709
4911
  if (name === "map") {
4710
4912
  return this.resolveMappedValue(action.args, context.currentValue, context.responseValue);
4711
4913
  }
@@ -4722,10 +4924,40 @@ var ActionRuntime = class {
4722
4924
  }
4723
4925
  return inputValue;
4724
4926
  }
4927
+ unwrapListValue(value) {
4928
+ return isListElementLike(value) ? value.descriptor : value;
4929
+ }
4930
+ getCurrentListState(currentValue) {
4931
+ if (!isListElementLike(currentValue) || typeof currentValue.listId !== "string") {
4932
+ return null;
4933
+ }
4934
+ return this.stateByKey.get(currentValue.listId) ?? this.stateById.get(currentValue.listId) ?? null;
4935
+ }
4936
+ cloneValue(value) {
4937
+ if (value === null || value === void 0) {
4938
+ return value;
4939
+ }
4940
+ if (typeof value === "object") {
4941
+ return structuredClone(value);
4942
+ }
4943
+ return value;
4944
+ }
4945
+ stringifyValue(value) {
4946
+ if (value == null) {
4947
+ return "";
4948
+ }
4949
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
4950
+ return String(value);
4951
+ }
4952
+ return JSON.stringify(value);
4953
+ }
4725
4954
  };
4726
4955
  function isIfElseArgs(value) {
4727
4956
  return typeof value === "object" && value !== null && !Array.isArray(value);
4728
4957
  }
4958
+ function isListElementLike(value) {
4959
+ 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;
4960
+ }
4729
4961
 
4730
4962
  // src/lib/dom-state.ts
4731
4963
  function isPlainObject3(value) {
@@ -4754,7 +4986,7 @@ function isElementInsidePendingResetModal(element, pendingResetModalIds) {
4754
4986
  }
4755
4987
  return false;
4756
4988
  }
4757
- function captureElementState(root, pendingResetModalIds, stateByKey) {
4989
+ function captureElementState(root, pendingResetModalIds) {
4758
4990
  const state = {
4759
4991
  activeId: null,
4760
4992
  values: /* @__PURE__ */ new Map(),
@@ -4782,20 +5014,6 @@ function captureElementState(root, pendingResetModalIds, stateByKey) {
4782
5014
  if (element instanceof HTMLInputElement && element.type === "checkbox" && element.id) {
4783
5015
  state.checked.set(element.id, element.checked);
4784
5016
  }
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
5017
  }
4800
5018
  for (const element of Array.from(root.querySelectorAll("[id], [data-widget-key]"))) {
4801
5019
  const top = element.scrollTop;
@@ -4817,16 +5035,44 @@ function captureElementState(root, pendingResetModalIds, stateByKey) {
4817
5035
  }
4818
5036
  return state;
4819
5037
  }
4820
- function restoreElementState(root, state) {
5038
+ function shouldRestoreTextValue(element, preservedValue, stateByKey) {
5039
+ const widgetKey = element.dataset.widgetKey;
5040
+ if (!widgetKey) {
5041
+ return true;
5042
+ }
5043
+ const widgetState = stateByKey.get(widgetKey);
5044
+ if (!widgetState || widgetState.widget !== "edit" && widgetState.widget !== "textarea") {
5045
+ return true;
5046
+ }
5047
+ return (widgetState.text ?? "") === preservedValue;
5048
+ }
5049
+ function shouldRestoreCheckedValue(element, preservedValue, stateByKey) {
5050
+ const widgetKey = element.dataset.widgetKey;
5051
+ if (!widgetKey) {
5052
+ return true;
5053
+ }
5054
+ const widgetState = stateByKey.get(widgetKey);
5055
+ if (!widgetState || widgetState.widget !== "checkbox") {
5056
+ return true;
5057
+ }
5058
+ return (widgetState.checked ?? false) === preservedValue;
5059
+ }
5060
+ function restoreElementState(root, state, stateByKey) {
4821
5061
  for (const [id, value] of state.values.entries()) {
4822
5062
  const element = root.querySelector(`#${CSS.escape(id)}`);
4823
5063
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
5064
+ if (!shouldRestoreTextValue(element, value, stateByKey)) {
5065
+ continue;
5066
+ }
4824
5067
  element.value = value;
4825
5068
  }
4826
5069
  }
4827
5070
  for (const [id, checked] of state.checked.entries()) {
4828
5071
  const element = root.querySelector(`#${CSS.escape(id)}`);
4829
5072
  if (element instanceof HTMLInputElement && element.type === "checkbox") {
5073
+ if (!shouldRestoreCheckedValue(element, checked, stateByKey)) {
5074
+ continue;
5075
+ }
4830
5076
  element.checked = checked;
4831
5077
  }
4832
5078
  }
@@ -5129,6 +5375,430 @@ var DEFAULT_STYLE_MAP = {
5129
5375
  employeeLinkDark: "display:flex; align-items:center; width:100%; height:100%; color:#b8d0ea; font-weight:600;"
5130
5376
  };
5131
5377
 
5378
+ // src/lib/voice-runtime.ts
5379
+ var MAX_CONCURRENT_PLAYERS = 5;
5380
+ var MAX_RECORDING_MS = 10 * 60 * 1e3;
5381
+ var MAX_LISTENING_SILENCE_MS = 60 * 60 * 1e3;
5382
+ var SILENCE_STOP_MS = 2e3;
5383
+ var SPEECH_THRESHOLD = 0.035;
5384
+ var RECORDING_MIME_CANDIDATES = [
5385
+ "audio/webm;codecs=opus",
5386
+ "audio/webm",
5387
+ "audio/mp4",
5388
+ "audio/mp4;codecs=mp4a.40.2",
5389
+ "audio/wav"
5390
+ ];
5391
+ function isPlayableAudioPayload(value) {
5392
+ return typeof value === "object" && value !== null;
5393
+ }
5394
+ function decodeBase64(base64) {
5395
+ const normalized = base64.includes(",") ? base64.slice(base64.indexOf(",") + 1) : base64;
5396
+ const binary = atob(normalized);
5397
+ const bytes = new Uint8Array(binary.length);
5398
+ for (let index = 0; index < binary.length; index += 1) {
5399
+ bytes[index] = binary.charCodeAt(index);
5400
+ }
5401
+ return bytes;
5402
+ }
5403
+ function detectMimeType(bytes) {
5404
+ if (bytes.length >= 4 && bytes[0] === 26 && bytes[1] === 69 && bytes[2] === 223 && bytes[3] === 163) {
5405
+ return "audio/webm";
5406
+ }
5407
+ if (bytes.length >= 12 && bytes[4] === 102 && bytes[5] === 116 && bytes[6] === 121 && bytes[7] === 112) {
5408
+ return "audio/mp4";
5409
+ }
5410
+ if (bytes.length >= 3 && bytes[0] === 73 && bytes[1] === 68 && bytes[2] === 51) {
5411
+ return "audio/mpeg";
5412
+ }
5413
+ if (bytes.length >= 2 && bytes[0] === 255 && (bytes[1] & 224) === 224) {
5414
+ return "audio/mpeg";
5415
+ }
5416
+ 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) {
5417
+ return "audio/wav";
5418
+ }
5419
+ if (bytes.length >= 4 && bytes[0] === 79 && bytes[1] === 103 && bytes[2] === 103 && bytes[3] === 83) {
5420
+ return "audio/ogg";
5421
+ }
5422
+ return "audio/mpeg";
5423
+ }
5424
+ function blobToBase64(blob) {
5425
+ return new Promise((resolve, reject) => {
5426
+ const reader = new FileReader();
5427
+ reader.onerror = () => reject(reader.error ?? new Error("Failed to read recorded audio data"));
5428
+ reader.onloadend = () => {
5429
+ const result = typeof reader.result === "string" ? reader.result : "";
5430
+ const base64 = result.includes(",") ? result.slice(result.indexOf(",") + 1) : result;
5431
+ resolve(base64);
5432
+ };
5433
+ reader.readAsDataURL(blob);
5434
+ });
5435
+ }
5436
+ var VoiceRuntime = class {
5437
+ debugLogging;
5438
+ triggerSystemEvent;
5439
+ activePlayers = [];
5440
+ mediaStream = null;
5441
+ mediaRecorder = null;
5442
+ recordingChunks = [];
5443
+ recordingTimeoutId = null;
5444
+ listening = false;
5445
+ listenContext = null;
5446
+ listenAnalyser = null;
5447
+ listenSource = null;
5448
+ listenFrameId = null;
5449
+ lastSpeechAt = 0;
5450
+ listeningStartedAt = 0;
5451
+ recordingStartedFromListening = false;
5452
+ speechEventTriggered = false;
5453
+ visibilityHandler;
5454
+ pageHideHandler;
5455
+ constructor(options) {
5456
+ this.debugLogging = options.debugLogging;
5457
+ this.triggerSystemEvent = options.triggerSystemEvent;
5458
+ this.visibilityHandler = () => {
5459
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
5460
+ void this.handlePageDeactivation();
5461
+ }
5462
+ };
5463
+ this.pageHideHandler = () => {
5464
+ void this.handlePageDeactivation();
5465
+ };
5466
+ if (typeof document !== "undefined") {
5467
+ document.addEventListener("visibilitychange", this.visibilityHandler);
5468
+ }
5469
+ if (typeof window !== "undefined") {
5470
+ window.addEventListener("pagehide", this.pageHideHandler);
5471
+ }
5472
+ }
5473
+ async play(value) {
5474
+ const payload = this.normalizePlayablePayload(value);
5475
+ if (!payload) {
5476
+ return;
5477
+ }
5478
+ const bytes = decodeBase64(payload.audioData);
5479
+ const copy = new Uint8Array(bytes.byteLength);
5480
+ copy.set(bytes);
5481
+ const blob = new Blob([copy.buffer], { type: payload.mimeType });
5482
+ const url = URL.createObjectURL(blob);
5483
+ const audio = new Audio(url);
5484
+ audio.preload = "auto";
5485
+ const player = { audio, url, payload };
5486
+ audio.onended = () => {
5487
+ void this.finishPlayback(player, "finished");
5488
+ };
5489
+ audio.onerror = () => {
5490
+ this.detachPlayer(player);
5491
+ logRuntimeError("voice.play", new Error("Audio playback failed"), { mimeType: payload.mimeType });
5492
+ };
5493
+ this.activePlayers.push(player);
5494
+ while (this.activePlayers.length > MAX_CONCURRENT_PLAYERS) {
5495
+ const oldest = this.activePlayers.shift();
5496
+ if (!oldest) {
5497
+ break;
5498
+ }
5499
+ oldest.audio.pause();
5500
+ void this.finishPlayback(oldest, "stopped");
5501
+ }
5502
+ logRuntimeDebug(this.debugLogging, "voice-play", {
5503
+ mimeType: payload.mimeType,
5504
+ bytes: bytes.byteLength,
5505
+ activePlayers: this.activePlayers.length
5506
+ });
5507
+ await audio.play();
5508
+ }
5509
+ async stopPlaying() {
5510
+ const players = [...this.activePlayers];
5511
+ for (const player of players) {
5512
+ player.audio.pause();
5513
+ player.audio.currentTime = 0;
5514
+ await this.finishPlayback(player, "stopped");
5515
+ }
5516
+ }
5517
+ async startRecording() {
5518
+ try {
5519
+ const stream = await this.ensureInputStream();
5520
+ this.startRecordingInternal(stream, false);
5521
+ } catch (error) {
5522
+ await this.handleRecordingError(error);
5523
+ }
5524
+ }
5525
+ stopRecording() {
5526
+ if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5527
+ return;
5528
+ }
5529
+ this.clearRecordingTimeout();
5530
+ this.mediaRecorder.stop();
5531
+ }
5532
+ async startListening() {
5533
+ if (this.listening) {
5534
+ return;
5535
+ }
5536
+ try {
5537
+ const stream = await this.ensureInputStream();
5538
+ const context = new AudioContext();
5539
+ const analyser = context.createAnalyser();
5540
+ analyser.fftSize = 2048;
5541
+ const source = context.createMediaStreamSource(stream);
5542
+ source.connect(analyser);
5543
+ this.listening = true;
5544
+ this.listenContext = context;
5545
+ this.listenAnalyser = analyser;
5546
+ this.listenSource = source;
5547
+ this.lastSpeechAt = 0;
5548
+ this.listeningStartedAt = performance.now();
5549
+ this.speechEventTriggered = false;
5550
+ const sampleBuffer = new Uint8Array(analyser.fftSize);
5551
+ const step = async () => {
5552
+ if (!this.listening || !this.listenAnalyser) {
5553
+ return;
5554
+ }
5555
+ this.listenAnalyser.getByteTimeDomainData(sampleBuffer);
5556
+ let squareSum = 0;
5557
+ for (const sample of sampleBuffer) {
5558
+ const centered = (sample - 128) / 128;
5559
+ squareSum += centered * centered;
5560
+ }
5561
+ const rms = Math.sqrt(squareSum / sampleBuffer.length);
5562
+ const now = performance.now();
5563
+ if (rms >= SPEECH_THRESHOLD) {
5564
+ this.lastSpeechAt = now;
5565
+ this.listeningStartedAt = now;
5566
+ if (!this.speechEventTriggered) {
5567
+ this.speechEventTriggered = true;
5568
+ await this.triggerSystemEvent("onSpeechDetected", null);
5569
+ }
5570
+ if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5571
+ this.startRecordingInternal(stream, true);
5572
+ }
5573
+ } else if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive" && this.lastSpeechAt > 0 && now - this.lastSpeechAt >= SILENCE_STOP_MS) {
5574
+ this.stopRecording();
5575
+ this.lastSpeechAt = 0;
5576
+ this.speechEventTriggered = false;
5577
+ } else if (!this.recordingStartedFromListening && now - this.listeningStartedAt >= MAX_LISTENING_SILENCE_MS) {
5578
+ await this.stopListening();
5579
+ return;
5580
+ }
5581
+ this.listenFrameId = window.requestAnimationFrame(() => {
5582
+ void step();
5583
+ });
5584
+ };
5585
+ void step();
5586
+ } catch (error) {
5587
+ await this.handleListeningError(error);
5588
+ }
5589
+ }
5590
+ async stopListening() {
5591
+ this.listening = false;
5592
+ this.speechEventTriggered = false;
5593
+ this.lastSpeechAt = 0;
5594
+ this.listeningStartedAt = 0;
5595
+ if (this.listenFrameId !== null) {
5596
+ window.cancelAnimationFrame(this.listenFrameId);
5597
+ this.listenFrameId = null;
5598
+ }
5599
+ try {
5600
+ await this.listenContext?.close();
5601
+ } catch (error) {
5602
+ logRuntimeError("voice.stopListening", error);
5603
+ }
5604
+ this.listenContext = null;
5605
+ this.listenAnalyser = null;
5606
+ this.listenSource = null;
5607
+ if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
5608
+ this.stopRecording();
5609
+ }
5610
+ this.recordingStartedFromListening = false;
5611
+ this.releaseInputStreamIfIdle();
5612
+ }
5613
+ dispose() {
5614
+ void this.stopListening();
5615
+ this.stopRecording();
5616
+ this.clearRecordingTimeout();
5617
+ void this.stopPlaying();
5618
+ if (typeof document !== "undefined") {
5619
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
5620
+ }
5621
+ if (typeof window !== "undefined") {
5622
+ window.removeEventListener("pagehide", this.pageHideHandler);
5623
+ }
5624
+ this.stopStreamTracks();
5625
+ }
5626
+ startRecordingInternal(stream, fromListening) {
5627
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
5628
+ return;
5629
+ }
5630
+ if (typeof MediaRecorder === "undefined") {
5631
+ throw new Error("MediaRecorder is not supported in this browser");
5632
+ }
5633
+ const preferredMimeType = this.getPreferredRecordingMimeType();
5634
+ const options = preferredMimeType ? { mimeType: preferredMimeType } : void 0;
5635
+ const recorder = options ? new MediaRecorder(stream, options) : new MediaRecorder(stream);
5636
+ this.mediaRecorder = recorder;
5637
+ this.recordingChunks = [];
5638
+ this.recordingStartedFromListening = fromListening;
5639
+ recorder.ondataavailable = (event) => {
5640
+ if (event.data && event.data.size > 0) {
5641
+ this.recordingChunks.push(event.data);
5642
+ }
5643
+ };
5644
+ recorder.onerror = (event) => {
5645
+ void this.handleRecordingError(event.error ?? new Error("Unknown recording error"));
5646
+ };
5647
+ recorder.onstart = () => {
5648
+ logRuntimeDebug(this.debugLogging, "voice-recording-started", {
5649
+ mimeType: recorder.mimeType || preferredMimeType || "unknown",
5650
+ fromListening
5651
+ });
5652
+ void this.triggerSystemEvent("onRecordingStarted", null);
5653
+ };
5654
+ recorder.onstop = () => {
5655
+ void this.handleRecorderStop(recorder);
5656
+ };
5657
+ recorder.start();
5658
+ this.clearRecordingTimeout();
5659
+ this.recordingTimeoutId = window.setTimeout(() => {
5660
+ void this.stopRecording();
5661
+ }, MAX_RECORDING_MS);
5662
+ }
5663
+ async handleRecorderStop(recorder) {
5664
+ this.clearRecordingTimeout();
5665
+ const mimeType = recorder.mimeType || this.getPreferredRecordingMimeType() || "audio/webm";
5666
+ const blob = new Blob(this.recordingChunks, { type: mimeType });
5667
+ this.recordingChunks = [];
5668
+ this.mediaRecorder = null;
5669
+ try {
5670
+ const audioData = await blobToBase64(blob);
5671
+ logRuntimeDebug(this.debugLogging, "voice-recording-stopped", {
5672
+ mimeType,
5673
+ size: blob.size,
5674
+ fromListening: this.recordingStartedFromListening
5675
+ });
5676
+ await this.triggerSystemEvent("onRecordingStopped", {
5677
+ audioData,
5678
+ mimeType
5679
+ });
5680
+ } catch (error) {
5681
+ await this.handleRecordingError(error);
5682
+ } finally {
5683
+ this.recordingStartedFromListening = false;
5684
+ this.releaseInputStreamIfIdle();
5685
+ }
5686
+ }
5687
+ async handleRecordingError(error) {
5688
+ logRuntimeError("voice.recording", error);
5689
+ await this.triggerSystemEvent("onRecordingError", this.normalizeErrorMessage(error));
5690
+ }
5691
+ async handleListeningError(error) {
5692
+ logRuntimeError("voice.listening", error);
5693
+ const message = this.normalizeErrorMessage(error);
5694
+ await this.triggerSystemEvent("onListeningError", message);
5695
+ await this.triggerSystemEvent("onListeringError", message);
5696
+ }
5697
+ normalizePlayablePayload(value) {
5698
+ if (typeof value === "string" && value.trim()) {
5699
+ const bytes = decodeBase64(value.trim());
5700
+ return {
5701
+ audioData: value.trim(),
5702
+ mimeType: detectMimeType(bytes)
5703
+ };
5704
+ }
5705
+ if (isPlayableAudioPayload(value) && typeof value.audioData === "string" && value.audioData.trim()) {
5706
+ const audioData = value.audioData.trim();
5707
+ const mimeType = typeof value.mimeType === "string" && value.mimeType.trim() ? value.mimeType.trim() : detectMimeType(decodeBase64(audioData));
5708
+ return { audioData, mimeType };
5709
+ }
5710
+ return null;
5711
+ }
5712
+ getPreferredRecordingMimeType() {
5713
+ if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
5714
+ return null;
5715
+ }
5716
+ for (const candidate of RECORDING_MIME_CANDIDATES) {
5717
+ if (MediaRecorder.isTypeSupported(candidate)) {
5718
+ return candidate;
5719
+ }
5720
+ }
5721
+ return null;
5722
+ }
5723
+ async ensureInputStream() {
5724
+ if (this.mediaStream && this.mediaStream.active) {
5725
+ return this.mediaStream;
5726
+ }
5727
+ if (!navigator.mediaDevices?.getUserMedia) {
5728
+ throw new Error("Audio input is not supported in this browser");
5729
+ }
5730
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
5731
+ return this.mediaStream;
5732
+ }
5733
+ clearRecordingTimeout() {
5734
+ if (this.recordingTimeoutId !== null) {
5735
+ window.clearTimeout(this.recordingTimeoutId);
5736
+ this.recordingTimeoutId = null;
5737
+ }
5738
+ }
5739
+ releaseInputStreamIfIdle() {
5740
+ if (this.listening) {
5741
+ return;
5742
+ }
5743
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
5744
+ return;
5745
+ }
5746
+ this.stopStreamTracks();
5747
+ }
5748
+ stopStreamTracks() {
5749
+ if (!this.mediaStream) {
5750
+ return;
5751
+ }
5752
+ for (const track of this.mediaStream.getTracks()) {
5753
+ track.stop();
5754
+ }
5755
+ this.mediaStream = null;
5756
+ }
5757
+ normalizeErrorMessage(error) {
5758
+ if (error instanceof Error) {
5759
+ return error.message;
5760
+ }
5761
+ return String(error);
5762
+ }
5763
+ detachPlayer(player) {
5764
+ const index = this.activePlayers.indexOf(player);
5765
+ if (index >= 0) {
5766
+ this.activePlayers.splice(index, 1);
5767
+ }
5768
+ player.audio.onended = null;
5769
+ player.audio.onerror = null;
5770
+ URL.revokeObjectURL(player.url);
5771
+ }
5772
+ async finishPlayback(player, reason) {
5773
+ if (!this.activePlayers.includes(player)) {
5774
+ return;
5775
+ }
5776
+ const payload = {
5777
+ audioData: player.payload.audioData,
5778
+ mimeType: player.payload.mimeType
5779
+ };
5780
+ this.detachPlayer(player);
5781
+ await this.triggerSystemEvent(reason === "finished" ? "onPlayFinished" : "onPlayingStopped", payload);
5782
+ }
5783
+ async handlePageDeactivation() {
5784
+ const hadActiveRecording = Boolean(this.mediaRecorder && this.mediaRecorder.state !== "inactive");
5785
+ const hadActiveListening = this.listening;
5786
+ if (!hadActiveRecording && !hadActiveListening) {
5787
+ return;
5788
+ }
5789
+ if (hadActiveRecording) {
5790
+ await this.handleRecordingError(new Error("Recording stopped because the browser tab became inactive"));
5791
+ this.stopRecording();
5792
+ }
5793
+ if (hadActiveListening) {
5794
+ await this.stopListening();
5795
+ if (!hadActiveRecording) {
5796
+ await this.handleListeningError(new Error("Listening stopped because the browser tab became inactive"));
5797
+ }
5798
+ }
5799
+ }
5800
+ };
5801
+
5132
5802
  // src/lib/widgets/adaptive-layout.ts
5133
5803
  var renderAdaptiveLayout = (env, node, _state, key, path, itemContext) => {
5134
5804
  const mobile = env.isMobileViewport();
@@ -5587,7 +6257,7 @@ var renderOverlayContainer = (env, node, _state, key, path, itemContext) => {
5587
6257
  var renderTabs = (env, node, state, key, path, itemContext) => {
5588
6258
  const tabs = node.tabs ?? [];
5589
6259
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
5590
- const rawActiveTab = typeof node.activeTab === "number" ? node.activeTab : state.activeTab ?? 0;
6260
+ const rawActiveTab = typeof state.activeTab === "number" ? state.activeTab : typeof node.activeTab === "number" ? node.activeTab : 0;
5591
6261
  const activeTab = Math.max(0, Math.min(rawActiveTab, Math.max(tabs.length - 1, 0)));
5592
6262
  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
6263
  const content = tabs[activeTab]?.content ? env.renderNode(tabs[activeTab].content, `${path}.tabs.${activeTab}`, itemContext) : "";
@@ -5945,8 +6615,6 @@ function bindDelegatedUi(root, env) {
5945
6615
  const state = env.stateByKey.get(key);
5946
6616
  if (state) {
5947
6617
  state.activeTab = Number.parseInt(tabButton.dataset.tabIndex ?? "0", 10) || 0;
5948
- const tabsNode = node;
5949
- tabsNode.activeTab = state.activeTab;
5950
6618
  }
5951
6619
  await env.rerenderRoot();
5952
6620
  return;
@@ -6183,12 +6851,14 @@ var RuntimeRenderer = class {
6183
6851
  stateByKey = /* @__PURE__ */ new Map();
6184
6852
  nodeById = /* @__PURE__ */ new Map();
6185
6853
  stateById = /* @__PURE__ */ new Map();
6854
+ vars = /* @__PURE__ */ new Map();
6186
6855
  resizeHandler = null;
6187
6856
  pointerHandler = null;
6188
6857
  eventSources = [];
6189
6858
  referenceRuntime;
6190
6859
  networkRuntime;
6191
6860
  actionRuntime;
6861
+ voiceRuntime;
6192
6862
  splitterDragState = null;
6193
6863
  hasTriggeredInitialRefresh = false;
6194
6864
  activeModalId = null;
@@ -6255,6 +6925,10 @@ var RuntimeRenderer = class {
6255
6925
  break;
6256
6926
  }
6257
6927
  },
6928
+ getVarValue: (name) => this.vars.get(name),
6929
+ setVarValue: (name, value) => {
6930
+ this.vars.set(name, value);
6931
+ },
6258
6932
  ensureWidgetState: (node, key) => this.ensureWidgetState(node, key),
6259
6933
  resolveItemNodeKey: (node, listKey, index, fallbackPath) => this.resolveItemNodeKey(node, listKey, index, fallbackPath),
6260
6934
  indexListElementNodes: (listNode, element, index) => this.indexListElementNodes(listNode, element, index)
@@ -6268,12 +6942,17 @@ var RuntimeRenderer = class {
6268
6942
  runActions: (actions, inputValue, context) => this.actionRuntime.runActions(actions, inputValue, context),
6269
6943
  rerenderRoot: () => this.rerenderRoot()
6270
6944
  });
6945
+ this.voiceRuntime = new VoiceRuntime({
6946
+ debugLogging: this.debugLogging,
6947
+ triggerSystemEvent: (eventName, inputValue) => this.triggerRuntimeSystemEvent(eventName, inputValue)
6948
+ });
6271
6949
  this.actionRuntime = new ActionRuntime({
6272
6950
  debugLogging: this.debugLogging,
6273
6951
  actionsMap: this.actionsMap,
6274
6952
  actionFunctions: this.actionFunctions,
6275
6953
  nodeById: this.nodeById,
6276
6954
  stateById: this.stateById,
6955
+ stateByKey: this.stateByKey,
6277
6956
  rerenderRoot: () => this.rerenderRoot(),
6278
6957
  dispatchWidgetEvent: (node, eventName, key, inputValue, pointer) => this.actionRuntime.dispatchWidgetEvent(node, eventName, key, inputValue, pointer),
6279
6958
  executeRequest: (requestName, currentValue) => this.networkRuntime.executeRequest(requestName, currentValue),
@@ -6283,6 +6962,14 @@ var RuntimeRenderer = class {
6283
6962
  resolveMappedValue: (template, currentValue, responseValue) => this.referenceRuntime.resolveMappedValue(template, currentValue, responseValue),
6284
6963
  setWidgetEnabled: (widgetId, enabled) => this.setWidgetEnabled(widgetId, enabled),
6285
6964
  clearWidget: (widgetId) => this.clearWidget(widgetId),
6965
+ clearListElementState: (listKey) => this.clearListElementState(listKey),
6966
+ focusWidget: (reference) => this.focusWidget(reference),
6967
+ playAudio: (value) => this.voiceRuntime.play(value),
6968
+ stopPlaying: () => this.voiceRuntime.stopPlaying(),
6969
+ startRecording: () => this.voiceRuntime.startRecording(),
6970
+ stopRecording: () => Promise.resolve(this.voiceRuntime.stopRecording()),
6971
+ startListening: () => this.voiceRuntime.startListening(),
6972
+ stopListening: () => this.voiceRuntime.stopListening(),
6286
6973
  setUiReference: (widgetId, resourceRef) => {
6287
6974
  const node = this.nodeById.get(widgetId);
6288
6975
  if (!node || node.widget !== "ui-reference") {
@@ -6376,6 +7063,7 @@ var RuntimeRenderer = class {
6376
7063
  source.close();
6377
7064
  }
6378
7065
  this.eventSources.length = 0;
7066
+ this.voiceRuntime.dispose();
6379
7067
  };
6380
7068
  }
6381
7069
  rerenderRoot() {
@@ -6383,7 +7071,7 @@ var RuntimeRenderer = class {
6383
7071
  return Promise.resolve();
6384
7072
  }
6385
7073
  this.reindexStaticTree();
6386
- const preservedState = captureElementState(this.root, this.pendingResetModalIds, this.stateByKey);
7074
+ const preservedState = captureElementState(this.root, this.pendingResetModalIds);
6387
7075
  this.root.innerHTML = this.renderMarkup();
6388
7076
  this.root.classList.toggle("vjt-root--mobile", isMobileViewport());
6389
7077
  this.root.classList.toggle("vjt-root--desktop", !isMobileViewport());
@@ -6392,7 +7080,7 @@ var RuntimeRenderer = class {
6392
7080
  }
6393
7081
  this.attachInputBehavior(this.root);
6394
7082
  this.attachWidgetEvents(this.root);
6395
- restoreElementState(this.root, preservedState);
7083
+ restoreElementState(this.root, preservedState, this.stateByKey);
6396
7084
  this.activeContextMenu = adjustContextMenuPosition(this.root, this.activeContextMenu);
6397
7085
  this.pendingResetModalIds.clear();
6398
7086
  if (typeof this.onRuntimeSnapshot === "function") {
@@ -6407,6 +7095,17 @@ var RuntimeRenderer = class {
6407
7095
  }
6408
7096
  await this.actionRuntime.runActions(actions, null, { currentValue: null });
6409
7097
  }
7098
+ async triggerRuntimeSystemEvent(eventName, inputValue) {
7099
+ const actions = this.systemEvents[eventName];
7100
+ if (!actions?.length) {
7101
+ return;
7102
+ }
7103
+ await this.actionRuntime.runActions(actions, inputValue, {
7104
+ currentValue: inputValue,
7105
+ responseValue: inputValue
7106
+ });
7107
+ await this.rerenderRoot();
7108
+ }
6410
7109
  async navigateTo(url) {
6411
7110
  if (typeof window === "undefined" || !url.trim()) {
6412
7111
  return;
@@ -6461,6 +7160,12 @@ var RuntimeRenderer = class {
6461
7160
  this.stateById.set(restoredState.id, restoredState);
6462
7161
  }
6463
7162
  }
7163
+ this.vars.clear();
7164
+ for (const [name, value] of snapshot.vars ?? []) {
7165
+ if (typeof name === "string" && name.length > 0) {
7166
+ this.vars.set(name, deepClone(value));
7167
+ }
7168
+ }
6464
7169
  if (snapshot.activeModalId && this.nodeById.get(snapshot.activeModalId)?.widget === "modal-window") {
6465
7170
  this.activeModalId = snapshot.activeModalId;
6466
7171
  }
@@ -6471,6 +7176,7 @@ var RuntimeRenderer = class {
6471
7176
  createRuntimeSnapshot() {
6472
7177
  return {
6473
7178
  stateByKey: Array.from(this.stateByKey.entries(), ([key, state]) => [key, deepClone(state)]),
7179
+ vars: Array.from(this.vars.entries(), ([name, value]) => [name, deepClone(value)]),
6474
7180
  activeModalId: this.activeModalId,
6475
7181
  activeContextMenu: this.activeContextMenu ? deepClone(this.activeContextMenu) : null
6476
7182
  };
@@ -7414,6 +8120,28 @@ var RuntimeRenderer = class {
7414
8120
  clearListElementState(listKey) {
7415
8121
  this.referenceRuntime.clearListElementState(listKey);
7416
8122
  }
8123
+ focusWidget(reference) {
8124
+ if (!(this.root instanceof HTMLElement)) {
8125
+ return;
8126
+ }
8127
+ const widgetId = reference.split(".")[0]?.trim();
8128
+ if (!widgetId) {
8129
+ return;
8130
+ }
8131
+ const directTarget = this.root.querySelector(`#${CSS.escape(widgetId)}`);
8132
+ if (directTarget && typeof directTarget.focus === "function") {
8133
+ directTarget.focus({ preventScroll: true });
8134
+ return;
8135
+ }
8136
+ const widgetRoot = this.root.querySelector(`[data-widget-id="${CSS.escape(widgetId)}"]`);
8137
+ if (!widgetRoot) {
8138
+ return;
8139
+ }
8140
+ const nestedTarget = widgetRoot.matches("input, textarea, select, button, a[href], [tabindex]") ? widgetRoot : widgetRoot.querySelector("input, textarea, select, button, a[href], [tabindex]");
8141
+ if (nestedTarget && typeof nestedTarget.focus === "function") {
8142
+ nestedTarget.focus({ preventScroll: true });
8143
+ }
8144
+ }
7417
8145
  };
7418
8146
  function renderJson(description, options = {}) {
7419
8147
  const renderer = new RuntimeRenderer(description, options);
@@ -6,6 +6,8 @@ export type ActionExecutionContext = {
6
6
  x: number;
7
7
  y: number;
8
8
  } | null;
9
+ currentList?: unknown[];
10
+ currentIndex?: number;
9
11
  };
10
12
  type ActionRuntimeOptions = {
11
13
  debugLogging: boolean;
@@ -13,6 +15,7 @@ type ActionRuntimeOptions = {
13
15
  actionFunctions: Record<string, () => unknown>;
14
16
  nodeById: Map<string, DescriptionNode>;
15
17
  stateById: Map<string, WidgetState>;
18
+ stateByKey: Map<string, WidgetState>;
16
19
  rerenderRoot: () => Promise<void>;
17
20
  dispatchWidgetEvent: (node: DescriptionNode, eventName: WidgetEventName, key: string, inputValue?: unknown, pointer?: {
18
21
  x: number;
@@ -27,6 +30,14 @@ type ActionRuntimeOptions = {
27
30
  resolveMappedValue: (template: unknown, currentValue: unknown, responseValue: unknown) => unknown;
28
31
  setWidgetEnabled: (widgetId: string, enabled: boolean) => void;
29
32
  clearWidget: (widgetId: string) => void;
33
+ clearListElementState: (listKey: string) => void;
34
+ focusWidget: (reference: string) => void;
35
+ playAudio: (value: unknown) => Promise<void>;
36
+ stopPlaying: () => Promise<void>;
37
+ startRecording: () => Promise<void>;
38
+ stopRecording: () => Promise<void>;
39
+ startListening: () => Promise<void>;
40
+ stopListening: () => Promise<void>;
30
41
  setUiReference: (widgetId: string, resourceRef: string) => void;
31
42
  refreshWidgetTree: (widgetId: string) => Promise<void>;
32
43
  renderWidgetTree: (widgetId: string) => Promise<void>;
@@ -54,6 +65,7 @@ export declare class ActionRuntime {
54
65
  private readonly actionFunctions;
55
66
  private readonly nodeById;
56
67
  private readonly stateById;
68
+ private readonly stateByKey;
57
69
  private readonly rerenderRoot;
58
70
  private readonly dispatchWidgetEventImpl;
59
71
  private readonly executeRequest;
@@ -63,6 +75,14 @@ export declare class ActionRuntime {
63
75
  private readonly resolveMappedValue;
64
76
  private readonly setWidgetEnabled;
65
77
  private readonly clearWidget;
78
+ private readonly clearListElementState;
79
+ private readonly focusWidget;
80
+ private readonly playAudio;
81
+ private readonly stopPlaying;
82
+ private readonly startRecording;
83
+ private readonly stopRecording;
84
+ private readonly startListening;
85
+ private readonly stopListening;
66
86
  private readonly setUiReference;
67
87
  private readonly refreshWidgetTree;
68
88
  private readonly renderWidgetTree;
@@ -82,5 +102,9 @@ export declare class ActionRuntime {
82
102
  getInlineActionsForNode(node: DescriptionNode): ActionDefinition[] | undefined;
83
103
  private runSingleAction;
84
104
  private executeAction;
105
+ private unwrapListValue;
106
+ private getCurrentListState;
107
+ private cloneValue;
108
+ private stringifyValue;
85
109
  }
86
110
  export {};
@@ -15,8 +15,8 @@ export type PreservedElementState = {
15
15
  }>;
16
16
  };
17
17
  export declare function isElementInsidePendingResetModal(element: HTMLElement, pendingResetModalIds: Set<string>): boolean;
18
- export declare function captureElementState(root: HTMLElement, pendingResetModalIds: Set<string>, stateByKey: Map<string, WidgetState>): PreservedElementState;
19
- export declare function restoreElementState(root: HTMLElement, state: PreservedElementState): void;
18
+ export declare function captureElementState(root: HTMLElement, pendingResetModalIds: Set<string>): PreservedElementState;
19
+ export declare function restoreElementState(root: HTMLElement, state: PreservedElementState, stateByKey: Map<string, WidgetState>): void;
20
20
  export declare function adjustContextMenuPosition(root: HTMLElement, activeContextMenu: {
21
21
  id: string;
22
22
  x: number;
@@ -25,6 +25,8 @@ export declare class NetworkRuntime {
25
25
  executeRequest(requestName: string, currentValue: unknown): Promise<unknown>;
26
26
  buildSchemaValue(schema: RequestSchema, currentValue: unknown, responseValue: unknown): Promise<unknown>;
27
27
  private stringifyPrimitive;
28
+ private createRequestError;
29
+ private toRequestErrorPayload;
28
30
  coercePrimitiveSchemaValue(type: PrimitiveRequestType, value: unknown): unknown;
29
31
  private handleSseEvent;
30
32
  }
@@ -6,6 +6,8 @@ type ReferenceRuntimeHost = {
6
6
  readonly nodeByKey: Map<string, DescriptionNode>;
7
7
  getAppValue: (name: string) => unknown;
8
8
  setAppValue: (name: string, value: unknown) => void;
9
+ getVarValue: (name: string) => unknown;
10
+ setVarValue: (name: string, value: unknown) => void;
9
11
  ensureWidgetState: (node: DescriptionNode, key: string) => WidgetState;
10
12
  resolveItemNodeKey: (node: DescriptionNode, listKey: string, index: number, fallbackPath: string) => string;
11
13
  indexListElementNodes: (listNode: ListNode | GridViewNode, element: DescriptionNode, index: number) => void;
@@ -4,7 +4,7 @@ export type TextAlign = 'left' | 'center' | 'right';
4
4
  export type VerticalAlign = 'top' | 'center' | 'bottom';
5
5
  export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
6
6
  export type WidgetEventName = 'onClick' | 'onUserValueChange' | 'onRefresh' | 'onEnter' | 'onShiftEnter' | 'onControlEnter';
7
- export type SystemEventName = 'onBeforeRender' | 'onAfterRender' | 'onBeforeNavigate' | 'onAfterNavigate';
7
+ export type SystemEventName = 'onBeforeRender' | 'onAfterRender' | 'onBeforeNavigate' | 'onAfterNavigate' | 'onSpeechDetected' | 'onRecordingStarted' | 'onRecordingStopped' | 'onRecordingError' | 'onListeningError' | 'onListeringError' | 'onPlayFinished' | 'onPlayingStopped';
8
8
  export type PrimitiveRequestType = 'int' | 'float' | 'boolean' | 'string';
9
9
  export type ActionDefinition = {
10
10
  action: string;
@@ -34,6 +34,7 @@ export type RequestDefinition = {
34
34
  request: RequestSchema;
35
35
  response?: RequestSchema;
36
36
  onResponse?: ActionDefinition[];
37
+ onError?: ActionDefinition[];
37
38
  };
38
39
  export type SseEventDefinition = {
39
40
  name: string;
@@ -114,6 +115,7 @@ export type WidgetState = {
114
115
  };
115
116
  export type RuntimeSnapshot = {
116
117
  stateByKey: Array<[string, WidgetState]>;
118
+ vars: Array<[string, unknown]>;
117
119
  activeModalId: string | null;
118
120
  activeContextMenu: {
119
121
  id: string;
@@ -0,0 +1,48 @@
1
+ import type { SystemEventName } from './types.js';
2
+ type VoiceRuntimeOptions = {
3
+ debugLogging: boolean;
4
+ triggerSystemEvent: (eventName: SystemEventName, inputValue: unknown) => Promise<void>;
5
+ };
6
+ export declare class VoiceRuntime {
7
+ private readonly debugLogging;
8
+ private readonly triggerSystemEvent;
9
+ private readonly activePlayers;
10
+ private mediaStream;
11
+ private mediaRecorder;
12
+ private recordingChunks;
13
+ private recordingTimeoutId;
14
+ private listening;
15
+ private listenContext;
16
+ private listenAnalyser;
17
+ private listenSource;
18
+ private listenFrameId;
19
+ private lastSpeechAt;
20
+ private listeningStartedAt;
21
+ private recordingStartedFromListening;
22
+ private speechEventTriggered;
23
+ private readonly visibilityHandler;
24
+ private readonly pageHideHandler;
25
+ constructor(options: VoiceRuntimeOptions);
26
+ play(value: unknown): Promise<void>;
27
+ stopPlaying(): Promise<void>;
28
+ startRecording(): Promise<void>;
29
+ stopRecording(): void;
30
+ startListening(): Promise<void>;
31
+ stopListening(): Promise<void>;
32
+ dispose(): void;
33
+ private startRecordingInternal;
34
+ private handleRecorderStop;
35
+ private handleRecordingError;
36
+ private handleListeningError;
37
+ private normalizePlayablePayload;
38
+ private getPreferredRecordingMimeType;
39
+ private ensureInputStream;
40
+ private clearRecordingTimeout;
41
+ private releaseInputStreamIfIdle;
42
+ private stopStreamTracks;
43
+ private normalizeErrorMessage;
44
+ private detachPlayer;
45
+ private finishPlayback;
46
+ private handlePageDeactivation;
47
+ }
48
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortexm/vjt",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",