@vortexm/vjt 0.1.13 → 0.1.14

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);
4609
4638
  }
4610
- if (template.includes("$ref:")) {
4611
- return template.replaceAll(/\$ref:([A-Za-z0-9_.]+)/g, (_match, ref) => {
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);
4647
+ }
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,8 @@ 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_PRE_ROLL_MS = 300;
5873
+ var LISTENING_RECORDER_TIMESLICE_MS = 100;
5787
5874
  var RECORDING_MIME_CANDIDATES = [
5788
5875
  "audio/webm;codecs=opus",
5789
5876
  "audio/webm",
@@ -5864,6 +5951,11 @@ var VoiceRuntime = class {
5864
5951
  listenAnalyser = null;
5865
5952
  listenSource = null;
5866
5953
  listenFrameId = null;
5954
+ listeningRecorder = null;
5955
+ listeningRecorderMimeType = "audio/webm";
5956
+ listeningPreRollChunks = [];
5957
+ listeningCaptureChunks = [];
5958
+ listeningCaptureActive = false;
5867
5959
  lastSpeechAt = 0;
5868
5960
  listeningStartedAt = 0;
5869
5961
  recordingStartedFromListening = false;
@@ -5898,15 +5990,37 @@ var VoiceRuntime = class {
5898
5990
  copy.set(bytes);
5899
5991
  const blob = new Blob([copy.buffer], { type: payload.mimeType });
5900
5992
  const url = URL.createObjectURL(blob);
5901
- const audio = new Audio(url);
5993
+ await this.playResolvedSource(url, payload, true, {
5994
+ mimeType: payload.mimeType,
5995
+ bytes: bytes.byteLength
5996
+ });
5997
+ }
5998
+ async playStatic(url) {
5999
+ if (typeof url !== "string" || !url.trim()) {
6000
+ return;
6001
+ }
6002
+ const safeUrl = sanitizeUrl(url.trim(), { allowRelative: true });
6003
+ if (!safeUrl) {
6004
+ logRuntimeError("voice.playStatic", new Error("Blocked unsafe audio url"), { url });
6005
+ return;
6006
+ }
6007
+ await this.playResolvedSource(safeUrl, {
6008
+ audioData: safeUrl,
6009
+ mimeType: "audio/url"
6010
+ }, false, {
6011
+ url: safeUrl
6012
+ });
6013
+ }
6014
+ async playResolvedSource(sourceUrl, payload, revokeUrl, debugDetails) {
6015
+ const audio = new Audio(sourceUrl);
5902
6016
  audio.preload = "auto";
5903
- const player = { audio, url, payload };
6017
+ const player = { audio, url: sourceUrl, revokeUrl, payload };
5904
6018
  audio.onended = () => {
5905
6019
  void this.finishPlayback(player, "finished");
5906
6020
  };
5907
6021
  audio.onerror = () => {
5908
6022
  this.detachPlayer(player);
5909
- logRuntimeError("voice.play", new Error("Audio playback failed"), { mimeType: payload.mimeType });
6023
+ logRuntimeError("voice.play", new Error("Audio playback failed"), debugDetails);
5910
6024
  };
5911
6025
  this.activePlayers.push(player);
5912
6026
  while (this.activePlayers.length > MAX_CONCURRENT_PLAYERS) {
@@ -5918,8 +6032,7 @@ var VoiceRuntime = class {
5918
6032
  void this.finishPlayback(oldest, "stopped");
5919
6033
  }
5920
6034
  logRuntimeDebug(this.debugLogging, "voice-play", {
5921
- mimeType: payload.mimeType,
5922
- bytes: bytes.byteLength,
6035
+ ...debugDetails,
5923
6036
  activePlayers: this.activePlayers.length
5924
6037
  });
5925
6038
  await audio.play();
@@ -5941,6 +6054,10 @@ var VoiceRuntime = class {
5941
6054
  }
5942
6055
  }
5943
6056
  stopRecording() {
6057
+ if (this.listeningCaptureActive) {
6058
+ void this.stopListeningCapture();
6059
+ return;
6060
+ }
5944
6061
  if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5945
6062
  return;
5946
6063
  }
@@ -5959,6 +6076,7 @@ var VoiceRuntime = class {
5959
6076
  analyser.fftSize = 2048;
5960
6077
  const source = context.createMediaStreamSource(stream);
5961
6078
  source.connect(analyser);
6079
+ this.startListeningRecorder(stream);
5962
6080
  this.listening = true;
5963
6081
  this.listenContext = context;
5964
6082
  this.listenAnalyser = analyser;
@@ -5986,14 +6104,14 @@ var VoiceRuntime = class {
5986
6104
  this.speechEventTriggered = true;
5987
6105
  await this.triggerSystemEvent("onSpeechDetected", null);
5988
6106
  }
5989
- if (!this.mediaRecorder || this.mediaRecorder.state === "inactive") {
5990
- this.startRecordingInternal(stream, true);
6107
+ if (!this.listeningCaptureActive) {
6108
+ this.beginListeningCapture();
5991
6109
  }
5992
- } else if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive" && this.lastSpeechAt > 0 && now - this.lastSpeechAt >= SILENCE_STOP_MS) {
5993
- this.stopRecording();
6110
+ } else if (this.listeningCaptureActive && this.lastSpeechAt > 0 && now - this.lastSpeechAt >= SILENCE_STOP_MS) {
6111
+ await this.stopListeningCapture();
5994
6112
  this.lastSpeechAt = 0;
5995
6113
  this.speechEventTriggered = false;
5996
- } else if (!this.recordingStartedFromListening && now - this.listeningStartedAt >= MAX_LISTENING_SILENCE_MS) {
6114
+ } else if (!this.listeningCaptureActive && now - this.listeningStartedAt >= MAX_LISTENING_SILENCE_MS) {
5997
6115
  await this.stopListening();
5998
6116
  return;
5999
6117
  }
@@ -6026,9 +6144,13 @@ var VoiceRuntime = class {
6026
6144
  this.listenContext = null;
6027
6145
  this.listenAnalyser = null;
6028
6146
  this.listenSource = null;
6029
- if (this.recordingStartedFromListening && this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
6030
- this.stopRecording();
6147
+ if (this.listeningCaptureActive) {
6148
+ await this.stopListeningCapture();
6031
6149
  }
6150
+ this.stopListeningRecorder();
6151
+ this.listeningPreRollChunks = [];
6152
+ this.listeningCaptureChunks = [];
6153
+ this.listeningCaptureActive = false;
6032
6154
  this.recordingStartedFromListening = false;
6033
6155
  this.releaseInputStreamIfIdle();
6034
6156
  }
@@ -6108,6 +6230,9 @@ var VoiceRuntime = class {
6108
6230
  }
6109
6231
  async handleRecordingError(error) {
6110
6232
  logRuntimeError("voice.recording", error);
6233
+ this.listeningCaptureActive = false;
6234
+ this.listeningCaptureChunks = [];
6235
+ this.recordingStartedFromListening = false;
6111
6236
  await this.triggerSystemEvent("onRecordingError", this.normalizeErrorMessage(error));
6112
6237
  }
6113
6238
  async handleListeningError(error) {
@@ -6152,6 +6277,91 @@ var VoiceRuntime = class {
6152
6277
  this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
6153
6278
  return this.mediaStream;
6154
6279
  }
6280
+ startListeningRecorder(stream) {
6281
+ if (this.listeningRecorder && this.listeningRecorder.state !== "inactive") {
6282
+ return;
6283
+ }
6284
+ if (typeof MediaRecorder === "undefined") {
6285
+ throw new Error("MediaRecorder is not supported in this browser");
6286
+ }
6287
+ const preferredMimeType = this.getPreferredRecordingMimeType();
6288
+ const options = preferredMimeType ? { mimeType: preferredMimeType } : void 0;
6289
+ const recorder = options ? new MediaRecorder(stream, options) : new MediaRecorder(stream);
6290
+ this.listeningRecorder = recorder;
6291
+ this.listeningRecorderMimeType = recorder.mimeType || preferredMimeType || "audio/webm";
6292
+ this.listeningPreRollChunks = [];
6293
+ this.listeningCaptureChunks = [];
6294
+ recorder.ondataavailable = (event) => {
6295
+ if (!event.data || event.data.size === 0) {
6296
+ return;
6297
+ }
6298
+ const timestamp = performance.now();
6299
+ if (this.listeningCaptureActive) {
6300
+ this.listeningCaptureChunks.push(event.data);
6301
+ return;
6302
+ }
6303
+ this.listeningPreRollChunks.push({ data: event.data, timestamp });
6304
+ const cutoff = timestamp - LISTENING_PRE_ROLL_MS;
6305
+ while (this.listeningPreRollChunks.length > 0 && this.listeningPreRollChunks[0].timestamp < cutoff) {
6306
+ this.listeningPreRollChunks.shift();
6307
+ }
6308
+ };
6309
+ recorder.onerror = (event) => {
6310
+ void this.handleRecordingError(event.error ?? new Error("Unknown listening recorder error"));
6311
+ };
6312
+ recorder.start(LISTENING_RECORDER_TIMESLICE_MS);
6313
+ }
6314
+ stopListeningRecorder() {
6315
+ if (!this.listeningRecorder) {
6316
+ return;
6317
+ }
6318
+ const recorder = this.listeningRecorder;
6319
+ this.listeningRecorder = null;
6320
+ recorder.ondataavailable = null;
6321
+ recorder.onerror = null;
6322
+ if (recorder.state !== "inactive") {
6323
+ recorder.stop();
6324
+ }
6325
+ }
6326
+ beginListeningCapture() {
6327
+ this.listeningCaptureActive = true;
6328
+ this.recordingStartedFromListening = true;
6329
+ this.listeningCaptureChunks = this.listeningPreRollChunks.map((chunk) => chunk.data);
6330
+ this.listeningPreRollChunks = [];
6331
+ logRuntimeDebug(this.debugLogging, "voice-recording-started", {
6332
+ mimeType: this.listeningRecorderMimeType,
6333
+ fromListening: true,
6334
+ preRollMs: LISTENING_PRE_ROLL_MS,
6335
+ preRollChunks: this.listeningCaptureChunks.length
6336
+ });
6337
+ void this.triggerSystemEvent("onRecordingStarted", null);
6338
+ }
6339
+ async stopListeningCapture() {
6340
+ if (!this.listeningCaptureActive) {
6341
+ return;
6342
+ }
6343
+ this.listeningCaptureActive = false;
6344
+ const chunks = [...this.listeningCaptureChunks];
6345
+ this.listeningCaptureChunks = [];
6346
+ try {
6347
+ const blob = new Blob(chunks, { type: this.listeningRecorderMimeType });
6348
+ const audioData = await blobToBase64(blob);
6349
+ logRuntimeDebug(this.debugLogging, "voice-recording-stopped", {
6350
+ mimeType: this.listeningRecorderMimeType,
6351
+ size: blob.size,
6352
+ fromListening: true,
6353
+ preRollMs: LISTENING_PRE_ROLL_MS
6354
+ });
6355
+ await this.triggerSystemEvent("onRecordingStopped", {
6356
+ audioData,
6357
+ mimeType: this.listeningRecorderMimeType
6358
+ });
6359
+ } catch (error) {
6360
+ await this.handleRecordingError(error);
6361
+ } finally {
6362
+ this.recordingStartedFromListening = false;
6363
+ }
6364
+ }
6155
6365
  clearRecordingTimeout() {
6156
6366
  if (this.recordingTimeoutId !== null) {
6157
6367
  window.clearTimeout(this.recordingTimeoutId);
@@ -6162,6 +6372,9 @@ var VoiceRuntime = class {
6162
6372
  if (this.listening) {
6163
6373
  return;
6164
6374
  }
6375
+ if (this.listeningRecorder && this.listeningRecorder.state !== "inactive") {
6376
+ return;
6377
+ }
6165
6378
  if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
6166
6379
  return;
6167
6380
  }
@@ -6189,7 +6402,9 @@ var VoiceRuntime = class {
6189
6402
  }
6190
6403
  player.audio.onended = null;
6191
6404
  player.audio.onerror = null;
6192
- URL.revokeObjectURL(player.url);
6405
+ if (player.revokeUrl) {
6406
+ URL.revokeObjectURL(player.url);
6407
+ }
6193
6408
  }
6194
6409
  async finishPlayback(player, reason) {
6195
6410
  if (!this.activePlayers.includes(player)) {
@@ -6203,7 +6418,9 @@ var VoiceRuntime = class {
6203
6418
  await this.triggerSystemEvent(reason === "finished" ? "onPlayFinished" : "onPlayingStopped", payload);
6204
6419
  }
6205
6420
  async handlePageDeactivation() {
6206
- const hadActiveRecording = Boolean(this.mediaRecorder && this.mediaRecorder.state !== "inactive");
6421
+ const hadActiveRecording = Boolean(
6422
+ this.mediaRecorder && this.mediaRecorder.state !== "inactive" || this.listeningCaptureActive
6423
+ );
6207
6424
  const hadActiveListening = this.listening;
6208
6425
  if (!hadActiveRecording && !hadActiveListening) {
6209
6426
  return;
@@ -6352,7 +6569,7 @@ function renderGridLayout(env, node, cellsVisible, path, itemContext) {
6352
6569
 
6353
6570
  // src/lib/widgets/conditional-container.ts
6354
6571
  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);
6572
+ const condition = typeof node.condition === "object" && node.condition !== null ? env.evaluateConditionExpression(node.condition, itemContext ?? null) : state.condition ?? env.normalizeBoolean(node.condition, true);
6356
6573
  const chosen = condition ? node.default : node.alternative;
6357
6574
  const content = chosen ? env.renderNode(chosen, `${path}.${condition ? "default" : "alternative"}`, itemContext) : "";
6358
6575
  const hasOwnBox = Boolean(
@@ -6585,10 +6802,12 @@ var renderButton = (env, node, state, key, _path, itemContext) => {
6585
6802
  const enabled = state.enabled ?? env.normalizeBoolean(node.enabled, true);
6586
6803
  const disabledAttribute = enabled ? "" : " disabled";
6587
6804
  const text = env.escapeHtml(env.resolveTextValue(state.text ?? node.text, itemContext));
6805
+ const hintValue = typeof node.hint === "string" ? env.resolveTextValue(node.hint, itemContext) : "";
6806
+ const hintAttribute = hintValue ? ` title="${env.escapeHtml(hintValue)}"` : "";
6588
6807
  const styleString = env.buildStyleString(node);
6589
6808
  const styleAttribute = styleString ? ` style="${env.escapeHtml(styleString)}"` : "";
6590
6809
  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>`;
6810
+ return `<button${common}${env.buildWidgetDataAttributes(key, node)} class="vjt-button" data-widget="button" type="${buttonType}"${styleAttribute}${hintAttribute}${disabledAttribute}>${text}</button>`;
6592
6811
  };
6593
6812
 
6594
6813
  // src/lib/widgets/checkbox.ts
@@ -7467,6 +7686,9 @@ var RuntimeRenderer = class {
7467
7686
  activeModalId = null;
7468
7687
  activeContextMenu = null;
7469
7688
  activeConfirmModal = null;
7689
+ activeToasts = [];
7690
+ toastCounter = 0;
7691
+ toastTimeoutIds = /* @__PURE__ */ new Map();
7470
7692
  lastPointer = { x: 24, y: 24 };
7471
7693
  pendingResetModalIds = /* @__PURE__ */ new Set();
7472
7694
  pendingScrollAction = null;
@@ -7544,6 +7766,7 @@ var RuntimeRenderer = class {
7544
7766
  setVarValue: (name, value) => {
7545
7767
  this.vars.set(name, value);
7546
7768
  },
7769
+ resolveI18nValue: (value) => this.resolveI18nValue(value),
7547
7770
  ensureWidgetState: (node, key) => this.ensureWidgetState(node, key),
7548
7771
  resolveItemNodeKey: (node, listKey, index, fallbackPath) => this.resolveItemNodeKey(node, listKey, index, fallbackPath),
7549
7772
  indexListElementNodes: (listNode, element, index) => this.indexListElementNodes(listNode, element, index)
@@ -7585,7 +7808,10 @@ var RuntimeRenderer = class {
7585
7808
  scrollWidgetToBottom: (reference) => this.scrollWidgetToBottom(reference),
7586
7809
  scrollWidgetToElementByIndex: (reference, index) => this.scrollWidgetToElementByIndex(reference, index),
7587
7810
  playAudio: (value) => this.voiceRuntime.play(value),
7811
+ playStaticAudio: (url) => this.voiceRuntime.playStatic(url),
7588
7812
  stopPlaying: () => this.voiceRuntime.stopPlaying(),
7813
+ showInfoToast: (message) => this.showToast("info", message),
7814
+ showErrorToast: (message) => this.showToast("error", message),
7589
7815
  copyToClipboard: (value) => this.copyToClipboard(value),
7590
7816
  selectFile: (args) => this.selectFile(args),
7591
7817
  confirmModal: (args, inputValue) => this.confirmModal(args, inputValue),
@@ -7705,6 +7931,10 @@ var RuntimeRenderer = class {
7705
7931
  source.close();
7706
7932
  }
7707
7933
  this.eventSources.length = 0;
7934
+ for (const timeoutId of this.toastTimeoutIds.values()) {
7935
+ window.clearTimeout(timeoutId);
7936
+ }
7937
+ this.toastTimeoutIds.clear();
7708
7938
  this.voiceRuntime.dispose();
7709
7939
  };
7710
7940
  }
@@ -7905,6 +8135,21 @@ var RuntimeRenderer = class {
7905
8135
  this.activeConfirmModal = null;
7906
8136
  activeModal.resolve(confirmed);
7907
8137
  }
8138
+ showToast(kind, message) {
8139
+ const text = message.trim();
8140
+ if (!text) {
8141
+ return;
8142
+ }
8143
+ const toastId = ++this.toastCounter;
8144
+ this.activeToasts.push({ id: toastId, kind, text });
8145
+ void this.rerenderRoot();
8146
+ const timeoutId = window.setTimeout(() => {
8147
+ this.toastTimeoutIds.delete(toastId);
8148
+ this.activeToasts = this.activeToasts.filter((entry) => entry.id !== toastId);
8149
+ void this.rerenderRoot();
8150
+ }, 3600);
8151
+ this.toastTimeoutIds.set(toastId, timeoutId);
8152
+ }
7908
8153
  reindexStaticTree() {
7909
8154
  this.nodeByKey.clear();
7910
8155
  this.nodeById.clear();
@@ -8325,7 +8570,8 @@ var RuntimeRenderer = class {
8325
8570
  name: node.name,
8326
8571
  enabled: normalizeBoolean(node.enabled, true),
8327
8572
  definitionSignature: buildDefinitionSignature(node.elements ?? []),
8328
- elements: deepClone(node.elements ?? [])
8573
+ elements: deepClone(node.elements ?? []),
8574
+ selected: toFiniteNumber3(node.item) ?? toFiniteNumber3(node.selected) ?? -1
8329
8575
  };
8330
8576
  break;
8331
8577
  case "listbox":
@@ -8423,6 +8669,7 @@ var RuntimeRenderer = class {
8423
8669
  if (state.widget === "list" || state.widget === "grid-view") {
8424
8670
  this.clearListElementState(state.key);
8425
8671
  state.elements = deepClone(node.elements ?? []);
8672
+ state.selected = toFiniteNumber3(node.item) ?? toFiniteNumber3(node.selected) ?? state.selected ?? -1;
8426
8673
  state.definitionSignature = nextSignature;
8427
8674
  }
8428
8675
  }
@@ -8602,7 +8849,18 @@ var RuntimeRenderer = class {
8602
8849
  const modalMarkup = this.activeModalId ? renderModalOverlay(env, this.activeModalId) : "";
8603
8850
  const menuMarkup = this.activeContextMenu ? renderContextMenuOverlay(env, this.activeContextMenu) : "";
8604
8851
  const confirmMarkup = this.activeConfirmModal ? renderConfirmModalOverlay(env, this.activeConfirmModal) : "";
8605
- return `${modalMarkup}${menuMarkup}${confirmMarkup}`;
8852
+ const toastMarkup = this.renderToastOverlay();
8853
+ return `${modalMarkup}${menuMarkup}${confirmMarkup}${toastMarkup}`;
8854
+ }
8855
+ renderToastOverlay() {
8856
+ if (this.activeToasts.length === 0) {
8857
+ return "";
8858
+ }
8859
+ const items = this.activeToasts.map((toast) => {
8860
+ const kindClass = toast.kind === "error" ? " vjt-toast--error" : " vjt-toast--info";
8861
+ return `<div class="vjt-toast${kindClass}" role="status" aria-live="polite">${escapeHtml(toast.text)}</div>`;
8862
+ }).join("");
8863
+ return `<div class="vjt-toast-stack">${items}</div>`;
8606
8864
  }
8607
8865
  updatePointerFromEvent(event) {
8608
8866
  this.lastPointer = updatePointerFromMouseEvent(event);
@@ -8712,6 +8970,7 @@ var RuntimeRenderer = class {
8712
8970
  case "grid-view":
8713
8971
  this.clearListElementState(state.key);
8714
8972
  state.elements = [];
8973
+ state.selected = -1;
8715
8974
  break;
8716
8975
  case "listbox":
8717
8976
  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 listeningRecorder;
20
+ private listeningRecorderMimeType;
21
+ private listeningPreRollChunks;
22
+ private listeningCaptureChunks;
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,10 @@ export declare class VoiceRuntime {
37
44
  private normalizePlayablePayload;
38
45
  private getPreferredRecordingMimeType;
39
46
  private ensureInputStream;
47
+ private startListeningRecorder;
48
+ private stopListeningRecorder;
49
+ private beginListeningCapture;
50
+ private stopListeningCapture;
40
51
  private clearRecordingTimeout;
41
52
  private releaseInputStreamIfIdle;
42
53
  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.14",
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
+ }