@vortexm/vjt 0.1.13 → 0.1.15

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.js CHANGED
@@ -4220,6 +4220,20 @@ function stringifyReferenceValue(value) {
4220
4220
  }
4221
4221
  return JSON.stringify(value);
4222
4222
  }
4223
+ function resolveInlineI18nValue(host, value) {
4224
+ if (typeof value !== "string") {
4225
+ return value;
4226
+ }
4227
+ if (value.startsWith("i18n:") && !value.slice(5).includes(" ")) {
4228
+ return host.resolveI18nValue(value);
4229
+ }
4230
+ if (!value.includes("i18n:")) {
4231
+ return value;
4232
+ }
4233
+ return value.replaceAll(/i18n:([A-Za-z0-9_.-]+)/g, (_match, key) => {
4234
+ return host.resolveI18nValue(`i18n:${key}`);
4235
+ });
4236
+ }
4223
4237
  function isReferenceValueEmpty(value) {
4224
4238
  if (value === null || value === void 0) {
4225
4239
  return true;
@@ -4274,6 +4288,10 @@ var ReferenceRuntime = class {
4274
4288
  constructor(host) {
4275
4289
  this.host = host;
4276
4290
  }
4291
+ getListSelectionIndex(listId) {
4292
+ const listState = this.host.stateByKey.get(listId) ?? this.host.stateById.get(listId);
4293
+ return listState?.selected ?? -1;
4294
+ }
4277
4295
  resolveScopedRootReference(scope, path, currentValue, responseValue, inputValue) {
4278
4296
  if (scope === "current") {
4279
4297
  if (path.length === 0) {
@@ -4388,6 +4406,9 @@ var ReferenceRuntime = class {
4388
4406
  }
4389
4407
  resolveCurrentReference(reference, currentValue) {
4390
4408
  if (this.isListElementContext(currentValue)) {
4409
+ if (reference === "isSelected") {
4410
+ return currentValue.index === this.getListSelectionIndex(currentValue.listId);
4411
+ }
4391
4412
  const resolvedDescriptor = this.resolveListDescriptorNode(currentValue.descriptor, currentValue);
4392
4413
  const firstPart = reference.split(".")[0];
4393
4414
  const namedNode = this.findNamedWidget(resolvedDescriptor, firstPart, currentValue);
@@ -4395,6 +4416,9 @@ var ReferenceRuntime = class {
4395
4416
  const widgetKey = this.host.resolveItemNodeKey(namedNode, currentValue.listId, currentValue.index, `named.${firstPart}`);
4396
4417
  const state = this.host.ensureWidgetState(namedNode, widgetKey);
4397
4418
  const rest = reference.split(".").slice(1).join(".");
4419
+ if (rest === "isSelected") {
4420
+ return currentValue.index === this.getListSelectionIndex(currentValue.listId);
4421
+ }
4398
4422
  return rest ? this.readWidgetField(state, rest) : state;
4399
4423
  }
4400
4424
  const directDescriptorValue = readPath(currentValue.descriptor, reference.split("."));
@@ -4420,9 +4444,9 @@ var ReferenceRuntime = class {
4420
4444
  readWidgetField(state, field) {
4421
4445
  switch (field) {
4422
4446
  case "text":
4423
- return state.text ?? "";
4447
+ return resolveInlineI18nValue(this.host, state.text ?? "");
4424
4448
  case "placeholder":
4425
- return state.placeholder ?? "";
4449
+ return resolveInlineI18nValue(this.host, state.placeholder ?? "");
4426
4450
  case "checked":
4427
4451
  return state.checked ?? false;
4428
4452
  case "enabled":
@@ -4433,6 +4457,8 @@ var ReferenceRuntime = class {
4433
4457
  return state.condition ?? false;
4434
4458
  case "selected":
4435
4459
  return state.selected ?? 0;
4460
+ case "item":
4461
+ return state.selected ?? (state.widget === "list" || state.widget === "grid-view" ? -1 : 0);
4436
4462
  case "value":
4437
4463
  if (state.widget === "combobox") {
4438
4464
  const index = state.selected ?? 0;
@@ -4454,9 +4480,9 @@ var ReferenceRuntime = class {
4454
4480
  case "isHidden":
4455
4481
  return state.isHidden ?? true;
4456
4482
  case "url":
4457
- return state.url ?? "";
4483
+ return resolveInlineI18nValue(this.host, state.url ?? "");
4458
4484
  case "base64":
4459
- return state.base64 ?? "";
4485
+ return resolveInlineI18nValue(this.host, state.base64 ?? "");
4460
4486
  case "elements":
4461
4487
  if (state.widget === "list") {
4462
4488
  return (state.elements ?? []).map((descriptor, index) => ({
@@ -4538,6 +4564,9 @@ var ReferenceRuntime = class {
4538
4564
  case "selected":
4539
4565
  state.selected = toFiniteNumber(inputValue) ?? 0;
4540
4566
  break;
4567
+ case "item":
4568
+ state.selected = toFiniteNumber(inputValue) ?? 0;
4569
+ break;
4541
4570
  case "isHidden":
4542
4571
  state.isHidden = Boolean(inputValue);
4543
4572
  break;
@@ -4604,15 +4633,25 @@ var ReferenceRuntime = class {
4604
4633
  }
4605
4634
  resolveMappedValue(template, currentValue, responseValue, inputValue = currentValue) {
4606
4635
  if (typeof template === "string") {
4607
- if (template.startsWith("$ref:")) {
4608
- return this.resolveReference(template.slice(5), currentValue, responseValue, inputValue);
4636
+ if (template.startsWith("i18n:") && !template.slice(5).includes(" ")) {
4637
+ return this.host.resolveI18nValue(template);
4638
+ }
4639
+ let resolvedTemplate = template;
4640
+ if (resolvedTemplate.includes("i18n:")) {
4641
+ resolvedTemplate = resolvedTemplate.replaceAll(/i18n:([A-Za-z0-9_.-]+)/g, (_match, key) => {
4642
+ return this.host.resolveI18nValue(`i18n:${key}`);
4643
+ });
4644
+ }
4645
+ if (resolvedTemplate.startsWith("$ref:")) {
4646
+ return this.resolveReference(resolvedTemplate.slice(5), currentValue, responseValue, inputValue);
4609
4647
  }
4610
- if (template.includes("$ref:")) {
4611
- return template.replaceAll(/\$ref:([A-Za-z0-9_.]+)/g, (_match, ref) => {
4648
+ if (resolvedTemplate.includes("$ref:")) {
4649
+ return resolvedTemplate.replaceAll(/\$ref:([A-Za-z0-9_.]+)/g, (_match, ref) => {
4612
4650
  const resolved = this.resolveReference(ref, currentValue, responseValue, inputValue);
4613
4651
  return stringifyReferenceValue(resolved);
4614
4652
  });
4615
4653
  }
4654
+ return resolvedTemplate;
4616
4655
  }
4617
4656
  if (Array.isArray(template)) {
4618
4657
  return template.map((entry) => this.resolveMappedValue(entry, currentValue, responseValue, inputValue));
@@ -4748,7 +4787,10 @@ var ActionRuntime = class {
4748
4787
  scrollWidgetToBottom;
4749
4788
  scrollWidgetToElementByIndex;
4750
4789
  playAudio;
4790
+ playStaticAudio;
4751
4791
  stopPlaying;
4792
+ showInfoToast;
4793
+ showErrorToast;
4752
4794
  copyToClipboard;
4753
4795
  selectFile;
4754
4796
  confirmModal;
@@ -4790,7 +4832,10 @@ var ActionRuntime = class {
4790
4832
  this.scrollWidgetToBottom = options.scrollWidgetToBottom;
4791
4833
  this.scrollWidgetToElementByIndex = options.scrollWidgetToElementByIndex;
4792
4834
  this.playAudio = options.playAudio;
4835
+ this.playStaticAudio = options.playStaticAudio;
4793
4836
  this.stopPlaying = options.stopPlaying;
4837
+ this.showInfoToast = options.showInfoToast;
4838
+ this.showErrorToast = options.showErrorToast;
4794
4839
  this.copyToClipboard = options.copyToClipboard;
4795
4840
  this.selectFile = options.selectFile;
4796
4841
  this.confirmModal = options.confirmModal;
@@ -4911,14 +4956,37 @@ var ActionRuntime = class {
4911
4956
  await this.navigateTo(name.slice(9));
4912
4957
  return null;
4913
4958
  }
4959
+ if (name.startsWith("pause ")) {
4960
+ const durationMs = Math.max(0, Math.trunc(this.toNumber(name.slice(6))));
4961
+ await new Promise((resolve) => {
4962
+ if (typeof window === "undefined") {
4963
+ setTimeout(resolve, durationMs);
4964
+ return;
4965
+ }
4966
+ window.setTimeout(resolve, durationMs);
4967
+ });
4968
+ return inputValue;
4969
+ }
4914
4970
  if (name.startsWith("play ")) {
4915
4971
  await this.playAudio(this.resolveReference(name.slice(5), context.currentValue, context.responseValue, inputValue));
4916
4972
  return null;
4917
4973
  }
4974
+ if (name.startsWith("playStatic ")) {
4975
+ await this.playStaticAudio(name.slice(11).trim());
4976
+ return null;
4977
+ }
4918
4978
  if (name === "copyToClipboard") {
4919
4979
  await this.copyToClipboard(inputValue);
4920
4980
  return inputValue;
4921
4981
  }
4982
+ if (name === "infoToast") {
4983
+ this.showInfoToast(this.stringifyValue(inputValue));
4984
+ return inputValue;
4985
+ }
4986
+ if (name === "errorToast") {
4987
+ this.showErrorToast(this.stringifyValue(inputValue));
4988
+ return inputValue;
4989
+ }
4922
4990
  if (name === "selectFile") {
4923
4991
  return this.selectFile(action.args);
4924
4992
  }
@@ -5002,6 +5070,18 @@ var ActionRuntime = class {
5002
5070
  this.moveCursorToEnd(name.slice(16));
5003
5071
  return null;
5004
5072
  }
5073
+ if (name.startsWith("setItem ")) {
5074
+ const reference = name.slice(8).trim();
5075
+ this.assignReference(`${reference}.item`, Math.max(-1, Math.trunc(this.toNumber(inputValue))), context.currentValue);
5076
+ return inputValue;
5077
+ }
5078
+ if (name === "setItemCurrent") {
5079
+ const current = context.currentValue;
5080
+ if (isListElementLike(current)) {
5081
+ this.assignReference(`${current.listId}.item`, Math.max(0, Math.trunc(current.index)), current);
5082
+ }
5083
+ return inputValue;
5084
+ }
5005
5085
  if (name.startsWith("scrollToTop ")) {
5006
5086
  this.scrollWidgetToTop(name.slice(12));
5007
5087
  return inputValue;
@@ -5109,7 +5189,8 @@ var ActionRuntime = class {
5109
5189
  if (name.startsWith("append ")) {
5110
5190
  const reference = name.slice(7);
5111
5191
  const currentText = this.resolveReference(reference, context.currentValue, context.responseValue, inputValue);
5112
- const nextValue = `${this.stringifyValue(currentText)}${this.stringifyValue(action.args)}`;
5192
+ const appendValue = this.resolveMappedValue(action.args, context.currentValue, context.responseValue, inputValue);
5193
+ const nextValue = `${this.stringifyValue(currentText)}${this.stringifyValue(appendValue)}`;
5113
5194
  this.assignReference(reference, nextValue, context.currentValue);
5114
5195
  return nextValue;
5115
5196
  }
@@ -5536,7 +5617,7 @@ function updatePointerFromMouseEvent(event) {
5536
5617
  };
5537
5618
  }
5538
5619
  function getListElementContextFromElement(element, stateByKey) {
5539
- const listElement = element.closest(".vjt-list-element");
5620
+ const listElement = element.closest(".vjt-list-element, .vjt-grid-cell");
5540
5621
  if (!(listElement instanceof HTMLElement)) {
5541
5622
  return null;
5542
5623
  }
@@ -5774,6 +5855,10 @@ var DEFAULT_STYLE_MAP = {
5774
5855
  spoilerFrameDark: "border: 1px solid rgba(109,143,179,0.2); background: rgba(255,255,255,0.035); border-radius: 10px;",
5775
5856
  clickableRowLight: "padding: 4px 8px; border-radius: 10px; border: 1px solid rgba(111, 143, 177, 0.16); background: rgba(255,255,255,0.32); cursor: pointer;",
5776
5857
  clickableRowDark: "padding: 4px 8px; border-radius: 10px; border: 1px solid rgba(109,143,179,0.14); background: rgba(255,255,255,0.03); cursor: pointer;",
5858
+ selectableTileLight: "padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(111, 143, 177, 0.18); background: rgba(255,255,255,0.34); cursor: pointer; transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;",
5859
+ selectableTileDark: "padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(109,143,179,0.16); background: rgba(255,255,255,0.03); cursor: pointer; transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;",
5860
+ selectableTileSelectedLight: "padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(87, 136, 191, 0.52); background: rgba(120, 169, 221, 0.18); box-shadow: 0 0 0 4px rgba(120, 169, 221, 0.12); cursor: pointer;",
5861
+ selectableTileSelectedDark: "padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(138, 186, 238, 0.48); background: rgba(83, 124, 168, 0.26); box-shadow: 0 0 0 4px rgba(83, 124, 168, 0.18); cursor: pointer;",
5777
5862
  employeeLinkLight: "display:flex; align-items:center; width:100%; height:100%; color:#54749a; font-weight:600;",
5778
5863
  employeeLinkDark: "display:flex; align-items:center; width:100%; height:100%; color:#b8d0ea; font-weight:600;"
5779
5864
  };
@@ -5784,6 +5869,9 @@ var MAX_RECORDING_MS = 10 * 60 * 1e3;
5784
5869
  var MAX_LISTENING_SILENCE_MS = 60 * 60 * 1e3;
5785
5870
  var SILENCE_STOP_MS = 2e3;
5786
5871
  var DEFAULT_SPEECH_THRESHOLD = 0.035;
5872
+ var LISTENING_TARGET_PRE_ROLL_MS = 300;
5873
+ var LISTENING_SEGMENT_MS = 1e3;
5874
+ var LISTENING_SEGMENT_STEP_MS = 500;
5787
5875
  var RECORDING_MIME_CANDIDATES = [
5788
5876
  "audio/webm;codecs=opus",
5789
5877
  "audio/webm",
@@ -5864,6 +5952,11 @@ var VoiceRuntime = class {
5864
5952
  listenAnalyser = null;
5865
5953
  listenSource = null;
5866
5954
  listenFrameId = null;
5955
+ listeningIdleSessions = [];
5956
+ listeningLaneTimeoutIds = [];
5957
+ listeningLaneIntervalIds = [];
5958
+ listeningPromotedSession = null;
5959
+ listeningCaptureActive = false;
5867
5960
  lastSpeechAt = 0;
5868
5961
  listeningStartedAt = 0;
5869
5962
  recordingStartedFromListening = false;
@@ -5898,15 +5991,37 @@ var VoiceRuntime = class {
5898
5991
  copy.set(bytes);
5899
5992
  const blob = new Blob([copy.buffer], { type: payload.mimeType });
5900
5993
  const url = URL.createObjectURL(blob);
5901
- const audio = new Audio(url);
5994
+ await this.playResolvedSource(url, payload, true, {
5995
+ mimeType: payload.mimeType,
5996
+ bytes: bytes.byteLength
5997
+ });
5998
+ }
5999
+ async playStatic(url) {
6000
+ if (typeof url !== "string" || !url.trim()) {
6001
+ return;
6002
+ }
6003
+ const safeUrl = sanitizeUrl(url.trim(), { allowRelative: true });
6004
+ if (!safeUrl) {
6005
+ logRuntimeError("voice.playStatic", new Error("Blocked unsafe audio url"), { url });
6006
+ return;
6007
+ }
6008
+ await this.playResolvedSource(safeUrl, {
6009
+ audioData: safeUrl,
6010
+ mimeType: "audio/url"
6011
+ }, false, {
6012
+ url: safeUrl
6013
+ });
6014
+ }
6015
+ async playResolvedSource(sourceUrl, payload, revokeUrl, debugDetails) {
6016
+ const audio = new Audio(sourceUrl);
5902
6017
  audio.preload = "auto";
5903
- const player = { audio, url, payload };
6018
+ const player = { audio, url: sourceUrl, revokeUrl, payload };
5904
6019
  audio.onended = () => {
5905
6020
  void this.finishPlayback(player, "finished");
5906
6021
  };
5907
6022
  audio.onerror = () => {
5908
6023
  this.detachPlayer(player);
5909
- logRuntimeError("voice.play", new Error("Audio playback failed"), { mimeType: payload.mimeType });
6024
+ logRuntimeError("voice.play", new Error("Audio playback failed"), debugDetails);
5910
6025
  };
5911
6026
  this.activePlayers.push(player);
5912
6027
  while (this.activePlayers.length > MAX_CONCURRENT_PLAYERS) {
@@ -5918,8 +6033,7 @@ var VoiceRuntime = class {
5918
6033
  void this.finishPlayback(oldest, "stopped");
5919
6034
  }
5920
6035
  logRuntimeDebug(this.debugLogging, "voice-play", {
5921
- mimeType: payload.mimeType,
5922
- bytes: bytes.byteLength,
6036
+ ...debugDetails,
5923
6037
  activePlayers: this.activePlayers.length
5924
6038
  });
5925
6039
  await audio.play();
@@ -5941,6 +6055,10 @@ var VoiceRuntime = class {
5941
6055
  }
5942
6056
  }
5943
6057
  stopRecording() {
6058
+ if (this.listeningCaptureActive) {
6059
+ void this.stopListeningCapture();
6060
+ return;
6061
+ }
5944
6062
  if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5945
6063
  return;
5946
6064
  }
@@ -5963,9 +6081,12 @@ var VoiceRuntime = class {
5963
6081
  this.listenContext = context;
5964
6082
  this.listenAnalyser = analyser;
5965
6083
  this.listenSource = source;
6084
+ this.listeningIdleSessions = [];
6085
+ this.listeningPromotedSession = null;
5966
6086
  this.lastSpeechAt = 0;
5967
6087
  this.listeningStartedAt = performance.now();
5968
6088
  this.speechEventTriggered = false;
6089
+ this.startListeningRecorderSchedule(stream);
5969
6090
  const sampleBuffer = new Uint8Array(analyser.fftSize);
5970
6091
  const step = async () => {
5971
6092
  if (!this.listening || !this.listenAnalyser) {
@@ -5986,14 +6107,14 @@ var VoiceRuntime = class {
5986
6107
  this.speechEventTriggered = true;
5987
6108
  await this.triggerSystemEvent("onSpeechDetected", null);
5988
6109
  }
5989
- if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5990
- this.startRecordingInternal(stream, true);
6110
+ if (!this.listeningCaptureActive) {
6111
+ this.beginListeningCapture();
5991
6112
  }
5992
- } else if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive" && this.lastSpeechAt > 0 && now - this.lastSpeechAt >= SILENCE_STOP_MS) {
5993
- this.stopRecording();
6113
+ } else if (this.listeningCaptureActive && this.lastSpeechAt > 0 && now - this.lastSpeechAt >= SILENCE_STOP_MS) {
6114
+ await this.stopListeningCapture();
5994
6115
  this.lastSpeechAt = 0;
5995
6116
  this.speechEventTriggered = false;
5996
- } else if (!this.recordingStartedFromListening && now - this.listeningStartedAt >= MAX_LISTENING_SILENCE_MS) {
6117
+ } else if (!this.listeningCaptureActive && now - this.listeningStartedAt >= MAX_LISTENING_SILENCE_MS) {
5997
6118
  await this.stopListening();
5998
6119
  return;
5999
6120
  }
@@ -6026,9 +6147,12 @@ var VoiceRuntime = class {
6026
6147
  this.listenContext = null;
6027
6148
  this.listenAnalyser = null;
6028
6149
  this.listenSource = null;
6029
- if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
6030
- this.stopRecording();
6150
+ if (this.listeningCaptureActive) {
6151
+ await this.stopListeningCapture();
6031
6152
  }
6153
+ this.clearListeningRecorderSchedule();
6154
+ await this.discardListeningIdleSessions();
6155
+ this.listeningCaptureActive = false;
6032
6156
  this.recordingStartedFromListening = false;
6033
6157
  this.releaseInputStreamIfIdle();
6034
6158
  }
@@ -6108,6 +6232,8 @@ var VoiceRuntime = class {
6108
6232
  }
6109
6233
  async handleRecordingError(error) {
6110
6234
  logRuntimeError("voice.recording", error);
6235
+ this.listeningCaptureActive = false;
6236
+ this.recordingStartedFromListening = false;
6111
6237
  await this.triggerSystemEvent("onRecordingError", this.normalizeErrorMessage(error));
6112
6238
  }
6113
6239
  async handleListeningError(error) {
@@ -6152,6 +6278,196 @@ var VoiceRuntime = class {
6152
6278
  this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
6153
6279
  return this.mediaStream;
6154
6280
  }
6281
+ startListeningRecorderSchedule(stream) {
6282
+ this.clearListeningRecorderSchedule();
6283
+ this.launchListeningRecorder(stream);
6284
+ const delayedStart = window.setTimeout(() => {
6285
+ if (!this.listening || this.listeningCaptureActive) {
6286
+ return;
6287
+ }
6288
+ this.launchListeningRecorder(stream);
6289
+ const laneB = window.setInterval(() => {
6290
+ if (!this.listening || this.listeningCaptureActive) {
6291
+ return;
6292
+ }
6293
+ this.launchListeningRecorder(stream);
6294
+ }, LISTENING_SEGMENT_MS);
6295
+ this.listeningLaneIntervalIds.push(laneB);
6296
+ }, LISTENING_SEGMENT_STEP_MS);
6297
+ this.listeningLaneTimeoutIds.push(delayedStart);
6298
+ const laneA = window.setInterval(() => {
6299
+ if (!this.listening || this.listeningCaptureActive) {
6300
+ return;
6301
+ }
6302
+ this.launchListeningRecorder(stream);
6303
+ }, LISTENING_SEGMENT_MS);
6304
+ this.listeningLaneIntervalIds.push(laneA);
6305
+ }
6306
+ clearListeningRecorderSchedule() {
6307
+ for (const timeoutId of this.listeningLaneTimeoutIds) {
6308
+ window.clearTimeout(timeoutId);
6309
+ }
6310
+ this.listeningLaneTimeoutIds = [];
6311
+ for (const intervalId of this.listeningLaneIntervalIds) {
6312
+ window.clearInterval(intervalId);
6313
+ }
6314
+ this.listeningLaneIntervalIds = [];
6315
+ }
6316
+ launchListeningRecorder(stream) {
6317
+ const session = this.createListeningRecorderSession(stream);
6318
+ this.listeningIdleSessions.push(session);
6319
+ session.recorder.start();
6320
+ session.stopTimeoutId = window.setTimeout(() => {
6321
+ if (!session.promoted && session.recorder.state !== "inactive") {
6322
+ session.recorder.stop();
6323
+ }
6324
+ }, LISTENING_SEGMENT_MS);
6325
+ }
6326
+ createListeningRecorderSession(stream) {
6327
+ if (typeof MediaRecorder === "undefined") {
6328
+ throw new Error("MediaRecorder is not supported in this browser");
6329
+ }
6330
+ const preferredMimeType = this.getPreferredRecordingMimeType();
6331
+ const options = preferredMimeType ? { mimeType: preferredMimeType } : void 0;
6332
+ const recorder = options ? new MediaRecorder(stream, options) : new MediaRecorder(stream);
6333
+ let resolveStopped = () => {
6334
+ };
6335
+ const stopped = new Promise((resolve) => {
6336
+ resolveStopped = resolve;
6337
+ });
6338
+ const session = {
6339
+ recorder,
6340
+ mimeType: recorder.mimeType || preferredMimeType || "audio/webm",
6341
+ startedAt: performance.now(),
6342
+ chunks: [],
6343
+ stopTimeoutId: null,
6344
+ promoted: false,
6345
+ discard: false,
6346
+ stopped,
6347
+ resolveStopped
6348
+ };
6349
+ recorder.ondataavailable = (event) => {
6350
+ if (event.data && event.data.size > 0) {
6351
+ session.chunks.push(event.data);
6352
+ }
6353
+ };
6354
+ recorder.onerror = (event) => {
6355
+ void this.handleRecordingError(event.error ?? new Error("Unknown listening recording error"));
6356
+ };
6357
+ recorder.onstop = () => {
6358
+ void this.handleListeningRecorderStop(session);
6359
+ };
6360
+ return session;
6361
+ }
6362
+ beginListeningCapture() {
6363
+ const winner = this.selectListeningRecorderWinner();
6364
+ if (!winner) {
6365
+ return;
6366
+ }
6367
+ this.listeningCaptureActive = true;
6368
+ this.recordingStartedFromListening = true;
6369
+ this.listeningPromotedSession = winner;
6370
+ winner.promoted = true;
6371
+ winner.discard = false;
6372
+ if (winner.stopTimeoutId !== null) {
6373
+ window.clearTimeout(winner.stopTimeoutId);
6374
+ winner.stopTimeoutId = null;
6375
+ }
6376
+ this.clearListeningRecorderSchedule();
6377
+ for (const session of this.listeningIdleSessions) {
6378
+ if (session === winner) {
6379
+ continue;
6380
+ }
6381
+ void this.discardListeningSession(session);
6382
+ }
6383
+ const preRollMs = Math.max(0, Math.round(performance.now() - winner.startedAt));
6384
+ logRuntimeDebug(this.debugLogging, "voice-recording-started", {
6385
+ mimeType: winner.mimeType,
6386
+ fromListening: true,
6387
+ preRollMs
6388
+ });
6389
+ void this.triggerSystemEvent("onRecordingStarted", null);
6390
+ }
6391
+ selectListeningRecorderWinner() {
6392
+ const activeSessions = this.listeningIdleSessions.filter((session) => session.recorder.state !== "inactive");
6393
+ if (activeSessions.length === 0) {
6394
+ return null;
6395
+ }
6396
+ const targetStartedAt = performance.now() - LISTENING_TARGET_PRE_ROLL_MS;
6397
+ const suitable = activeSessions.filter((session) => session.startedAt <= targetStartedAt).sort((left, right) => right.startedAt - left.startedAt);
6398
+ if (suitable.length > 0) {
6399
+ return suitable[0];
6400
+ }
6401
+ return activeSessions.sort((left, right) => left.startedAt - right.startedAt)[0] ?? null;
6402
+ }
6403
+ async stopListeningCapture() {
6404
+ if (!this.listeningCaptureActive || !this.listeningPromotedSession) {
6405
+ return;
6406
+ }
6407
+ const session = this.listeningPromotedSession;
6408
+ this.listeningCaptureActive = false;
6409
+ this.listeningPromotedSession = null;
6410
+ if (session.recorder.state !== "inactive") {
6411
+ session.recorder.stop();
6412
+ }
6413
+ await session.stopped;
6414
+ }
6415
+ async discardListeningSession(session) {
6416
+ session.discard = true;
6417
+ if (session.stopTimeoutId !== null) {
6418
+ window.clearTimeout(session.stopTimeoutId);
6419
+ session.stopTimeoutId = null;
6420
+ }
6421
+ if (session.recorder.state !== "inactive") {
6422
+ session.recorder.stop();
6423
+ }
6424
+ await session.stopped;
6425
+ }
6426
+ async discardListeningIdleSessions() {
6427
+ const sessions = [...this.listeningIdleSessions];
6428
+ for (const session of sessions) {
6429
+ if (session === this.listeningPromotedSession) {
6430
+ continue;
6431
+ }
6432
+ await this.discardListeningSession(session);
6433
+ }
6434
+ this.listeningIdleSessions = this.listeningPromotedSession ? [this.listeningPromotedSession] : [];
6435
+ }
6436
+ async handleListeningRecorderStop(session) {
6437
+ if (session.stopTimeoutId !== null) {
6438
+ window.clearTimeout(session.stopTimeoutId);
6439
+ session.stopTimeoutId = null;
6440
+ }
6441
+ this.listeningIdleSessions = this.listeningIdleSessions.filter((entry) => entry !== session);
6442
+ try {
6443
+ if (session.discard || !session.promoted) {
6444
+ return;
6445
+ }
6446
+ const blob = new Blob(session.chunks, { type: session.mimeType });
6447
+ const audioData = await blobToBase64(blob);
6448
+ logRuntimeDebug(this.debugLogging, "voice-recording-stopped", {
6449
+ mimeType: session.mimeType,
6450
+ size: blob.size,
6451
+ fromListening: true
6452
+ });
6453
+ await this.triggerSystemEvent("onRecordingStopped", {
6454
+ audioData,
6455
+ mimeType: session.mimeType
6456
+ });
6457
+ } catch (error) {
6458
+ await this.handleRecordingError(error);
6459
+ } finally {
6460
+ session.resolveStopped();
6461
+ if (session.promoted) {
6462
+ this.recordingStartedFromListening = false;
6463
+ if (this.listening && this.mediaStream?.active) {
6464
+ this.listeningStartedAt = performance.now();
6465
+ this.speechEventTriggered = false;
6466
+ this.startListeningRecorderSchedule(this.mediaStream);
6467
+ }
6468
+ }
6469
+ }
6470
+ }
6155
6471
  clearRecordingTimeout() {
6156
6472
  if (this.recordingTimeoutId !== null) {
6157
6473
  window.clearTimeout(this.recordingTimeoutId);
@@ -6189,7 +6505,9 @@ var VoiceRuntime = class {
6189
6505
  }
6190
6506
  player.audio.onended = null;
6191
6507
  player.audio.onerror = null;
6192
- URL.revokeObjectURL(player.url);
6508
+ if (player.revokeUrl) {
6509
+ URL.revokeObjectURL(player.url);
6510
+ }
6193
6511
  }
6194
6512
  async finishPlayback(player, reason) {
6195
6513
  if (!this.activePlayers.includes(player)) {
@@ -6203,7 +6521,9 @@ var VoiceRuntime = class {
6203
6521
  await this.triggerSystemEvent(reason === "finished" ? "onPlayFinished" : "onPlayingStopped", payload);
6204
6522
  }
6205
6523
  async handlePageDeactivation() {
6206
- const hadActiveRecording = Boolean(this.mediaRecorder && this.mediaRecorder.state !== "inactive");
6524
+ const hadActiveRecording = Boolean(
6525
+ this.mediaRecorder && this.mediaRecorder.state !== "inactive" || this.listeningCaptureActive
6526
+ );
6207
6527
  const hadActiveListening = this.listening;
6208
6528
  if (!hadActiveRecording && !hadActiveListening) {
6209
6529
  return;
@@ -6352,7 +6672,7 @@ function renderGridLayout(env, node, cellsVisible, path, itemContext) {
6352
6672
 
6353
6673
  // src/lib/widgets/conditional-container.ts
6354
6674
  var renderConditionalContainer = (env, node, state, key, path, itemContext) => {
6355
- const condition = typeof node.condition === "object" && node.condition !== null ? env.evaluateConditionExpression(node.condition, itemContext?.descriptor ?? null) : state.condition ?? env.normalizeBoolean(node.condition, true);
6675
+ const condition = typeof node.condition === "object" && node.condition !== null ? env.evaluateConditionExpression(node.condition, itemContext ?? null) : state.condition ?? env.normalizeBoolean(node.condition, true);
6356
6676
  const chosen = condition ? node.default : node.alternative;
6357
6677
  const content = chosen ? env.renderNode(chosen, `${path}.${condition ? "default" : "alternative"}`, itemContext) : "";
6358
6678
  const hasOwnBox = Boolean(
@@ -6585,10 +6905,12 @@ var renderButton = (env, node, state, key, _path, itemContext) => {
6585
6905
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6586
6906
  const disabledAttribute = enabled ? "" : " disabled";
6587
6907
  const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6908
+ const hintValue = typeof node.hint === "string" ? env.resolveTextValue(node.hint, itemContext) : "";
6909
+ const hintAttribute = hintValue ? ` title="${env.escapeHtml(hintValue)}"` : "";
6588
6910
  const styleString = env.buildStyleString(node);
6589
6911
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6590
6912
  const buttonType = env.escapeHtml(node.type === "submit" ? "submit" : "button");
6591
- return `<button${common}${env.buildWidgetDataAttributes(key, node)} class="vjt-button" data-widget="button" type="${buttonType}"${styleAttribute}${disabledAttribute}>${text}</button>`;
6913
+ return `<button${common}${env.buildWidgetDataAttributes(key, node)} class="vjt-button" data-widget="button" type="${buttonType}"${styleAttribute}${hintAttribute}${disabledAttribute}>${text}</button>`;
6592
6914
  };
6593
6915
 
6594
6916
  // src/lib/widgets/checkbox.ts
@@ -7467,6 +7789,9 @@ var RuntimeRenderer = class {
7467
7789
  activeModalId = null;
7468
7790
  activeContextMenu = null;
7469
7791
  activeConfirmModal = null;
7792
+ activeToasts = [];
7793
+ toastCounter = 0;
7794
+ toastTimeoutIds = /* @__PURE__ */ new Map();
7470
7795
  lastPointer = { x: 24, y: 24 };
7471
7796
  pendingResetModalIds = /* @__PURE__ */ new Set();
7472
7797
  pendingScrollAction = null;
@@ -7544,6 +7869,7 @@ var RuntimeRenderer = class {
7544
7869
  setVarValue: (name, value) => {
7545
7870
  this.vars.set(name, value);
7546
7871
  },
7872
+ resolveI18nValue: (value) => this.resolveI18nValue(value),
7547
7873
  ensureWidgetState: (node, key) => this.ensureWidgetState(node, key),
7548
7874
  resolveItemNodeKey: (node, listKey, index, fallbackPath) => this.resolveItemNodeKey(node, listKey, index, fallbackPath),
7549
7875
  indexListElementNodes: (listNode, element, index) => this.indexListElementNodes(listNode, element, index)
@@ -7585,7 +7911,10 @@ var RuntimeRenderer = class {
7585
7911
  scrollWidgetToBottom: (reference) => this.scrollWidgetToBottom(reference),
7586
7912
  scrollWidgetToElementByIndex: (reference, index) => this.scrollWidgetToElementByIndex(reference, index),
7587
7913
  playAudio: (value) => this.voiceRuntime.play(value),
7914
+ playStaticAudio: (url) => this.voiceRuntime.playStatic(url),
7588
7915
  stopPlaying: () => this.voiceRuntime.stopPlaying(),
7916
+ showInfoToast: (message) => this.showToast("info", message),
7917
+ showErrorToast: (message) => this.showToast("error", message),
7589
7918
  copyToClipboard: (value) => this.copyToClipboard(value),
7590
7919
  selectFile: (args) => this.selectFile(args),
7591
7920
  confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
@@ -7705,6 +8034,10 @@ var RuntimeRenderer = class {
7705
8034
  source.close();
7706
8035
  }
7707
8036
  this.eventSources.length = 0;
8037
+ for (const timeoutId of this.toastTimeoutIds.values()) {
8038
+ window.clearTimeout(timeoutId);
8039
+ }
8040
+ this.toastTimeoutIds.clear();
7708
8041
  this.voiceRuntime.dispose();
7709
8042
  };
7710
8043
  }
@@ -7905,6 +8238,21 @@ var RuntimeRenderer = class {
7905
8238
  this.activeConfirmModal = null;
7906
8239
  activeModal.resolve(confirmed);
7907
8240
  }
8241
+ showToast(kind, message) {
8242
+ const text = message.trim();
8243
+ if (!text) {
8244
+ return;
8245
+ }
8246
+ const toastId = ++this.toastCounter;
8247
+ this.activeToasts.push({ id: toastId, kind, text });
8248
+ void this.rerenderRoot();
8249
+ const timeoutId = window.setTimeout(() => {
8250
+ this.toastTimeoutIds.delete(toastId);
8251
+ this.activeToasts = this.activeToasts.filter((entry) => entry.id !== toastId);
8252
+ void this.rerenderRoot();
8253
+ }, 3600);
8254
+ this.toastTimeoutIds.set(toastId, timeoutId);
8255
+ }
7908
8256
  reindexStaticTree() {
7909
8257
  this.nodeByKey.clear();
7910
8258
  this.nodeById.clear();
@@ -8325,7 +8673,8 @@ var RuntimeRenderer = class {
8325
8673
  name: node.name,
8326
8674
  enabled: normalizeBoolean(node.enabled, true),
8327
8675
  definitionSignature: buildDefinitionSignature(node.elements ?? []),
8328
- elements: deepClone(node.elements ?? [])
8676
+ elements: deepClone(node.elements ?? []),
8677
+ selected: toFiniteNumber3(node.item) ?? toFiniteNumber3(node.selected) ?? -1
8329
8678
  };
8330
8679
  break;
8331
8680
  case "listbox":
@@ -8423,6 +8772,7 @@ var RuntimeRenderer = class {
8423
8772
  if (state.widget === "list" || state.widget === "grid-view") {
8424
8773
  this.clearListElementState(state.key);
8425
8774
  state.elements = deepClone(node.elements ?? []);
8775
+ state.selected = toFiniteNumber3(node.item) ?? toFiniteNumber3(node.selected) ?? state.selected ?? -1;
8426
8776
  state.definitionSignature = nextSignature;
8427
8777
  }
8428
8778
  }
@@ -8602,7 +8952,18 @@ var RuntimeRenderer = class {
8602
8952
  const modalMarkup = this.activeModalId ? renderModalOverlay(env, this.activeModalId) : "";
8603
8953
  const menuMarkup = this.activeContextMenu ? renderContextMenuOverlay(env, this.activeContextMenu) : "";
8604
8954
  const confirmMarkup = this.activeConfirmModal ? renderConfirmModalOverlay(env, this.activeConfirmModal) : "";
8605
- return `${modalMarkup}${menuMarkup}${confirmMarkup}`;
8955
+ const toastMarkup = this.renderToastOverlay();
8956
+ return `${modalMarkup}${menuMarkup}${confirmMarkup}${toastMarkup}`;
8957
+ }
8958
+ renderToastOverlay() {
8959
+ if (this.activeToasts.length === 0) {
8960
+ return "";
8961
+ }
8962
+ const items = this.activeToasts.map((toast) => {
8963
+ const kindClass = toast.kind === "error" ? " vjt-toast--error" : " vjt-toast--info";
8964
+ return `<div class="vjt-toast${kindClass}" role="status" aria-live="polite">${escapeHtml(toast.text)}</div>`;
8965
+ }).join("");
8966
+ return `<div class="vjt-toast-stack">${items}</div>`;
8606
8967
  }
8607
8968
  updatePointerFromEvent(event) {
8608
8969
  this.lastPointer = updatePointerFromMouseEvent(event);
@@ -8712,6 +9073,7 @@ var RuntimeRenderer = class {
8712
9073
  case "grid-view":
8713
9074
  this.clearListElementState(state.key);
8714
9075
  state.elements = [];
9076
+ state.selected = -1;
8715
9077
  break;
8716
9078
  case "listbox":
8717
9079
  state.listboxElements = [];
@@ -38,7 +38,10 @@ type ActionRuntimeOptions = {
38
38
  scrollWidgetToBottom: (reference: string) => void;
39
39
  scrollWidgetToElementByIndex: (reference: string, index: unknown) => void;
40
40
  playAudio: (value: unknown) => Promise<void>;
41
+ playStaticAudio: (url: string) => Promise<void>;
41
42
  stopPlaying: () => Promise<void>;
43
+ showInfoToast: (message: string) => void;
44
+ showErrorToast: (message: string) => void;
42
45
  copyToClipboard: (value: unknown) => Promise<void>;
43
46
  selectFile: (options: unknown) => Promise<unknown>;
44
47
  confirmModal: (options: unknown, inputValue: unknown) => Promise<boolean>;
@@ -93,7 +96,10 @@ export declare class ActionRuntime {
93
96
  private readonly scrollWidgetToBottom;
94
97
  private readonly scrollWidgetToElementByIndex;
95
98
  private readonly playAudio;
99
+ private readonly playStaticAudio;
96
100
  private readonly stopPlaying;
101
+ private readonly showInfoToast;
102
+ private readonly showErrorToast;
97
103
  private readonly copyToClipboard;
98
104
  private readonly selectFile;
99
105
  private readonly confirmModal;
@@ -10,6 +10,7 @@ type ReferenceRuntimeHost = {
10
10
  getVarValue: (name: string) => unknown;
11
11
  getVarsSnapshot: () => Record<string, unknown>;
12
12
  setVarValue: (name: string, value: unknown) => void;
13
+ resolveI18nValue: (value: string | undefined) => string;
13
14
  ensureWidgetState: (node: DescriptionNode, key: string) => WidgetState;
14
15
  resolveItemNodeKey: (node: DescriptionNode, listKey: string, index: number, fallbackPath: string) => string;
15
16
  indexListElementNodes: (listNode: ListNode | GridViewNode, element: DescriptionNode, index: number) => void;
@@ -17,6 +18,7 @@ type ReferenceRuntimeHost = {
17
18
  export declare class ReferenceRuntime {
18
19
  private readonly host;
19
20
  constructor(host: ReferenceRuntimeHost);
21
+ private getListSelectionIndex;
20
22
  private resolveScopedRootReference;
21
23
  private readVarReference;
22
24
  private writeVarReference;
@@ -16,6 +16,11 @@ export declare class VoiceRuntime {
16
16
  private listenAnalyser;
17
17
  private listenSource;
18
18
  private listenFrameId;
19
+ private listeningIdleSessions;
20
+ private listeningLaneTimeoutIds;
21
+ private listeningLaneIntervalIds;
22
+ private listeningPromotedSession;
23
+ private listeningCaptureActive;
19
24
  private lastSpeechAt;
20
25
  private listeningStartedAt;
21
26
  private recordingStartedFromListening;
@@ -24,6 +29,8 @@ export declare class VoiceRuntime {
24
29
  private readonly pageHideHandler;
25
30
  constructor(options: VoiceRuntimeOptions);
26
31
  play(value: unknown): Promise<void>;
32
+ playStatic(url: string): Promise<void>;
33
+ private playResolvedSource;
27
34
  stopPlaying(): Promise<void>;
28
35
  startRecording(): Promise<void>;
29
36
  stopRecording(): void;
@@ -37,6 +44,16 @@ export declare class VoiceRuntime {
37
44
  private normalizePlayablePayload;
38
45
  private getPreferredRecordingMimeType;
39
46
  private ensureInputStream;
47
+ private startListeningRecorderSchedule;
48
+ private clearListeningRecorderSchedule;
49
+ private launchListeningRecorder;
50
+ private createListeningRecorderSession;
51
+ private beginListeningCapture;
52
+ private selectListeningRecorderWinner;
53
+ private stopListeningCapture;
54
+ private discardListeningSession;
55
+ private discardListeningIdleSessions;
56
+ private handleListeningRecorderStop;
40
57
  private clearRecordingTimeout;
41
58
  private releaseInputStreamIfIdle;
42
59
  private stopStreamTracks;
@@ -3,6 +3,7 @@ import type { WidgetRenderer } from './context.js';
3
3
  export type ButtonNode = BaseNode & {
4
4
  widget: 'button';
5
5
  text?: string;
6
+ hint?: string;
6
7
  enabled?: boolean | string;
7
8
  type?: 'button' | 'submit';
8
9
  color?: 'bright' | 'regular';
@@ -4,6 +4,7 @@ export type GridViewNode = BaseNode & {
4
4
  widget: 'grid-view';
5
5
  rowSize?: number | string;
6
6
  size?: number;
7
+ item?: number | string;
7
8
  elements?: DescriptionNode[];
8
9
  };
9
10
  export declare const renderGridView: WidgetRenderer<GridViewNode>;
@@ -4,6 +4,7 @@ export type ListNode = BaseNode & {
4
4
  widget: 'list';
5
5
  orientation?: 'vertical' | 'horizontal';
6
6
  size?: number;
7
+ item?: number | string;
7
8
  elements?: DescriptionNode[];
8
9
  };
9
10
  export declare const renderList: WidgetRenderer<ListNode>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortexm/vjt",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/vjt-styles.css CHANGED
@@ -862,3 +862,61 @@ body {
862
862
  .vjt-confirm-actions .vjt-button {
863
863
  min-width: 120px;
864
864
  }
865
+
866
+ .vjt-toast-stack {
867
+ position: fixed;
868
+ left: 50%;
869
+ bottom: 56px;
870
+ transform: translateX(-50%);
871
+ z-index: 1200;
872
+ display: flex;
873
+ flex-direction: column;
874
+ align-items: center;
875
+ gap: 12px;
876
+ pointer-events: none;
877
+ }
878
+
879
+ .vjt-toast {
880
+ min-width: 320px;
881
+ max-width: min(680px, calc(100vw - 28px));
882
+ padding: 16px 20px;
883
+ border-radius: 18px;
884
+ border: 1px solid var(--vjt-border);
885
+ background: var(--vjt-surface);
886
+ color: var(--vjt-text);
887
+ box-shadow: var(--vjt-shadow);
888
+ line-height: 1.5;
889
+ white-space: pre-wrap;
890
+ font-size: 17px;
891
+ pointer-events: auto;
892
+ user-select: text;
893
+ -webkit-user-select: text;
894
+ }
895
+
896
+ .vjt-toast--info {
897
+ background: color-mix(in srgb, var(--vjt-surface) 88%, var(--vjt-accent) 12%);
898
+ }
899
+
900
+ .vjt-toast--error {
901
+ border-color: color-mix(in srgb, #d84f4f 55%, var(--vjt-border));
902
+ background: color-mix(in srgb, var(--vjt-surface) 80%, #a01f1f 20%);
903
+ }
904
+
905
+ @media (max-width: 720px) {
906
+ .vjt-toast-stack {
907
+ left: 16px;
908
+ right: 16px;
909
+ bottom: 32px;
910
+ transform: none;
911
+ align-items: stretch;
912
+ }
913
+
914
+ .vjt-toast {
915
+ min-width: 0;
916
+ max-width: none;
917
+ width: 100%;
918
+ padding: 18px 20px;
919
+ border-radius: 20px;
920
+ font-size: 18px;
921
+ }
922
+ }