@getlupa/client 1.18.3 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12588,7 +12588,7 @@ var __async = (__this, __arguments, generator) => {
12588
12588
  return "";
12589
12589
  }
12590
12590
  const transformedStr = typeof str === "string" ? str : str.toString();
12591
- return transformedStr.normalize === void 0 ? (_a = transformedStr.toLocaleLowerCase()) == null ? void 0 : _a.trim() : (_b = transformedStr.toLocaleLowerCase().normalize("NFKD").replace(/[^\w\s.-_/]/g, "")) == null ? void 0 : _b.trim();
12591
+ return transformedStr.normalize === void 0 ? (_a = transformedStr.toLocaleLowerCase()) == null ? void 0 : _a.trim() : (_b = transformedStr.toLocaleLowerCase().normalize("NFKD").replace(/[^\p{L}\p{N}\s]/gu, "")) == null ? void 0 : _b.trim();
12592
12592
  };
12593
12593
  const getTransformedString = (str) => {
12594
12594
  var _a, _b;
@@ -12619,6 +12619,11 @@ var __async = (__this, __arguments, generator) => {
12619
12619
  }
12620
12620
  return result2;
12621
12621
  };
12622
+ const getSocketClientId = () => {
12623
+ const timestamp = Date.now().toString(36);
12624
+ const randomString = getRandomString(8);
12625
+ return `${timestamp}-${randomString}`;
12626
+ };
12622
12627
  const toFixedIfNecessary = (value, precision = 2) => {
12623
12628
  return (+parseFloat(value).toFixed(precision)).toString();
12624
12629
  };
@@ -12733,6 +12738,10 @@ var __async = (__this, __arguments, generator) => {
12733
12738
  links: {
12734
12739
  searchResults: "/search"
12735
12740
  },
12741
+ voiceSearch: {
12742
+ enabled: false,
12743
+ queryKey: ""
12744
+ },
12736
12745
  panels: [
12737
12746
  {
12738
12747
  type: "suggestion",
@@ -14314,6 +14323,387 @@ var __async = (__this, __arguments, generator) => {
14314
14323
  resetHighlightIndex
14315
14324
  };
14316
14325
  });
14326
+ const Env = {
14327
+ production: "https://api.lupasearch.com/v1/",
14328
+ staging: "https://api.staging.lupasearch.com/v1/"
14329
+ };
14330
+ const VoiceServiceEnv = {
14331
+ production: "wss://voice.lupasearch.com",
14332
+ staging: "wss://voice.lupasearch.dev"
14333
+ };
14334
+ const DEFAULT_REQUEST_CONFIG = {
14335
+ method: "POST",
14336
+ headers: { "Content-Type": "application/json" }
14337
+ };
14338
+ const DEFAULT_HEADERS = DEFAULT_REQUEST_CONFIG.headers;
14339
+ const getVoiceServiceApiUrl = (environment, customVoiceServiceUrl) => {
14340
+ if (customVoiceServiceUrl) {
14341
+ return customVoiceServiceUrl;
14342
+ }
14343
+ return VoiceServiceEnv[environment] || VoiceServiceEnv["production"];
14344
+ };
14345
+ const getApiUrl = (environment, customBaseUrl) => {
14346
+ if (customBaseUrl) {
14347
+ return customBaseUrl;
14348
+ }
14349
+ return Env[environment] || Env["production"];
14350
+ };
14351
+ const _sfc_main$1z = /* @__PURE__ */ defineComponent({
14352
+ __name: "VoiceSearchProgressCircle",
14353
+ props: {
14354
+ isRecording: { type: Boolean },
14355
+ timesliceLimit: {},
14356
+ timeSliceLength: {}
14357
+ },
14358
+ setup(__props, { expose: __expose }) {
14359
+ const props = __props;
14360
+ const progressBar = ref(null);
14361
+ const getProgressBarColor = (progressBarStyle) => {
14362
+ if (!progressBarStyle.backgroundImage.startsWith("conic-gradient")) {
14363
+ return progressBarStyle.backgroundColor;
14364
+ }
14365
+ const colorStops = progressBarStyle.backgroundImage.replace(/conic-gradient\(|\)$/g, "").split(")");
14366
+ if (colorStops.length > 1) {
14367
+ return `${colorStops[0]})`;
14368
+ } else {
14369
+ return progressBarStyle.backgroundColor;
14370
+ }
14371
+ };
14372
+ const startProgressBar = () => {
14373
+ if (!progressBar.value) {
14374
+ return;
14375
+ }
14376
+ const duration = props.timesliceLimit * props.timeSliceLength;
14377
+ const progressBarStyle = window.getComputedStyle(progressBar.value);
14378
+ const progressBarColor = getProgressBarColor(progressBarStyle);
14379
+ progressBar.value.style.background = `conic-gradient(${progressBarColor} 0%, transparent 0%)`;
14380
+ let startTime = null;
14381
+ function updateProgress(timestamp) {
14382
+ if (!progressBar.value || !props.isRecording) {
14383
+ return;
14384
+ }
14385
+ if (!startTime)
14386
+ startTime = timestamp;
14387
+ const elapsed = timestamp - startTime;
14388
+ const progress = Math.min(elapsed / duration, 1) * 100;
14389
+ progressBar.value.style.background = `conic-gradient(${progressBarColor} ${progress}%, transparent ${progress}%)`;
14390
+ if (elapsed < duration) {
14391
+ requestAnimationFrame(updateProgress);
14392
+ }
14393
+ }
14394
+ requestAnimationFrame(updateProgress);
14395
+ };
14396
+ const stopProgressBar = () => {
14397
+ if (!progressBar.value) {
14398
+ return;
14399
+ }
14400
+ progressBar.value.style.background = "";
14401
+ };
14402
+ __expose({
14403
+ startProgressBar,
14404
+ stopProgressBar
14405
+ });
14406
+ return (_ctx, _cache) => {
14407
+ return openBlock(), createElementBlock("div", {
14408
+ ref_key: "progressBar",
14409
+ ref: progressBar,
14410
+ class: "lupa-progress-circle"
14411
+ }, null, 512);
14412
+ };
14413
+ }
14414
+ });
14415
+ const buildSocketMessageFrameHeader = (event, payloadLength) => {
14416
+ const headerObj = { event, length: payloadLength };
14417
+ const headerJson = JSON.stringify(headerObj);
14418
+ const headerBytes = new TextEncoder().encode(headerJson);
14419
+ const headerLength = new Uint32Array([headerBytes.length]);
14420
+ const headerLengthBytes = new Uint8Array(headerLength.buffer);
14421
+ const result2 = new Uint8Array(4 + headerBytes.length);
14422
+ result2.set(headerLengthBytes, 0);
14423
+ result2.set(headerBytes, 4);
14424
+ return result2;
14425
+ };
14426
+ function useVoiceRecorder(options) {
14427
+ const socket = ref(null);
14428
+ const mediaStream = ref(null);
14429
+ const mediaRecorder = ref(null);
14430
+ const isRecording = ref(false);
14431
+ const isSocketReady = ref(false);
14432
+ const errorRef = ref(null);
14433
+ const transcription = ref("");
14434
+ const timeSliceLength = computed(() => {
14435
+ var _a;
14436
+ return (_a = options.timesliceLength) != null ? _a : 1e3;
14437
+ });
14438
+ onBeforeUnmount(() => {
14439
+ closeSocket();
14440
+ stopRecording();
14441
+ });
14442
+ const initSocket = (url, onMessage) => {
14443
+ socket.value = new WebSocket(url);
14444
+ socket.value.onmessage = (event) => __async2(this, null, function* () {
14445
+ var _a;
14446
+ try {
14447
+ const msg = JSON.parse(event.data);
14448
+ if (msg.event === "ready") {
14449
+ if (((_a = mediaRecorder.value) == null ? void 0 : _a.state) !== "recording") {
14450
+ try {
14451
+ isSocketReady.value = true;
14452
+ yield startRecording();
14453
+ } catch (error) {
14454
+ console.error("Recording failed to start:", error);
14455
+ closeSocket();
14456
+ }
14457
+ }
14458
+ } else if (msg.event === "transcription") {
14459
+ transcription.value = msg.transcription;
14460
+ onMessage == null ? void 0 : onMessage(msg.transcription);
14461
+ stopSocketConnection();
14462
+ } else if (msg.event === "error") {
14463
+ errorRef.value = msg.message || "An error occurred during transcription";
14464
+ isSocketReady.value = false;
14465
+ stopRecording();
14466
+ closeSocket();
14467
+ }
14468
+ } catch (error) {
14469
+ console.error("Error processing WebSocket message:", error);
14470
+ }
14471
+ });
14472
+ socket.value.onclose = (event) => {
14473
+ if (event.code === 4001) {
14474
+ errorRef.value = event.reason || "Connection closed by server";
14475
+ }
14476
+ stopRecording();
14477
+ };
14478
+ socket.value.onerror = () => {
14479
+ errorRef.value = "Service connection error";
14480
+ stopRecording();
14481
+ };
14482
+ };
14483
+ const onMediaRecorderDataAvailable = (event) => __async2(this, null, function* () {
14484
+ var _a, _b;
14485
+ if (!isSocketReady.value || ((_a = socket.value) == null ? void 0 : _a.readyState) !== WebSocket.OPEN) {
14486
+ console.warn("Skipping audio chunk: socket not ready.");
14487
+ return;
14488
+ }
14489
+ const audioBuffer = yield event.data.arrayBuffer();
14490
+ const header = buildSocketMessageFrameHeader("audio-chunk", audioBuffer.byteLength);
14491
+ const buffer = new Uint8Array(header.length + audioBuffer.byteLength);
14492
+ buffer.set(header, 0);
14493
+ buffer.set(new Uint8Array(audioBuffer), header.length);
14494
+ (_b = socket.value) == null ? void 0 : _b.send(buffer);
14495
+ });
14496
+ const startRecording = () => __async2(this, null, function* () {
14497
+ var _a, _b;
14498
+ try {
14499
+ mediaStream.value = yield navigator.mediaDevices.getUserMedia({
14500
+ video: false,
14501
+ audio: {
14502
+ channelCount: 1,
14503
+ echoCancellation: true,
14504
+ sampleRate: options.sampleRate || 16e3
14505
+ }
14506
+ });
14507
+ mediaRecorder.value = new MediaRecorder(mediaStream.value, {
14508
+ mimeType: "audio/webm; codecs=opus"
14509
+ });
14510
+ mediaRecorder.value.ondataavailable = onMediaRecorderDataAvailable;
14511
+ mediaRecorder.value.start(timeSliceLength.value);
14512
+ isRecording.value = true;
14513
+ } catch (error) {
14514
+ if (error.name === "NotAllowedError") {
14515
+ errorRef.value = ((_a = options.labels) == null ? void 0 : _a.microphoneNotAllowed) || "Microphone access denied. Please allow microphone access in your browser settings.";
14516
+ } else if (error.name === "NotFoundError") {
14517
+ errorRef.value = ((_b = options.labels) == null ? void 0 : _b.microphoneNotFound) || "No microphone found. Please connect a microphone and try again.";
14518
+ }
14519
+ }
14520
+ });
14521
+ const stopRecording = () => {
14522
+ var _a, _b;
14523
+ (_a = mediaRecorder.value) == null ? void 0 : _a.stop();
14524
+ (_b = mediaStream.value) == null ? void 0 : _b.getTracks().forEach((track2) => {
14525
+ track2.stop();
14526
+ });
14527
+ isRecording.value = false;
14528
+ };
14529
+ const stopSocketConnection = () => {
14530
+ if (socket.value && socket.value.readyState === WebSocket.OPEN) {
14531
+ const endHeader = buildSocketMessageFrameHeader("audio-chunk-end", 0);
14532
+ socket.value.send(endHeader);
14533
+ setTimeout(() => {
14534
+ closeSocket();
14535
+ }, 1e3);
14536
+ }
14537
+ };
14538
+ const closeSocket = () => {
14539
+ var _a;
14540
+ (_a = socket.value) == null ? void 0 : _a.close();
14541
+ socket.value = null;
14542
+ isSocketReady.value = false;
14543
+ };
14544
+ const reset = () => {
14545
+ stopRecording();
14546
+ closeSocket();
14547
+ transcription.value = "";
14548
+ errorRef.value = null;
14549
+ isRecording.value = false;
14550
+ isSocketReady.value = false;
14551
+ };
14552
+ return {
14553
+ isRecording,
14554
+ isSocketReady,
14555
+ transcription,
14556
+ errorRef,
14557
+ initSocket,
14558
+ startRecording,
14559
+ stopRecording,
14560
+ stopSocketConnection,
14561
+ reset,
14562
+ closeSocket
14563
+ };
14564
+ }
14565
+ const _hoisted_1$1l = {
14566
+ key: 0,
14567
+ class: "lupa-dialog-overlay"
14568
+ };
14569
+ const _hoisted_2$W = { class: "lupa-dialog-content" };
14570
+ const _hoisted_3$F = { class: "lupa-listening-text" };
14571
+ const _hoisted_4$v = { class: "lupa-mic-button-wrapper" };
14572
+ const _sfc_main$1y = /* @__PURE__ */ defineComponent({
14573
+ __name: "VoiceSearchDialog",
14574
+ props: {
14575
+ isOpen: { type: Boolean },
14576
+ options: {}
14577
+ },
14578
+ emits: [
14579
+ "close",
14580
+ "transcript-update",
14581
+ "stop-recognize"
14582
+ ],
14583
+ setup(__props, { expose: __expose, emit: emit2 }) {
14584
+ const props = __props;
14585
+ const optionsStore = useOptionsStore();
14586
+ const {
14587
+ isRecording,
14588
+ isSocketReady,
14589
+ transcription,
14590
+ errorRef,
14591
+ initSocket,
14592
+ stopSocketConnection,
14593
+ reset
14594
+ } = useVoiceRecorder(props.options);
14595
+ const clientId = ref(null);
14596
+ const voiceSearchProgressBar = ref(null);
14597
+ const timesliceLimit = computed(() => {
14598
+ var _a;
14599
+ return (_a = props.options.timesliceLimit) != null ? _a : 4;
14600
+ });
14601
+ const timeSliceLength = computed(() => {
14602
+ var _a;
14603
+ return (_a = props.options.timesliceLength) != null ? _a : 1e3;
14604
+ });
14605
+ const stopDelay = computed(() => {
14606
+ var _a;
14607
+ return (_a = props.options.stopDelay) != null ? _a : 700;
14608
+ });
14609
+ const labels = computed(() => {
14610
+ var _a;
14611
+ return (_a = props.options.labels) != null ? _a : {};
14612
+ });
14613
+ const description = computed(() => {
14614
+ var _a, _b;
14615
+ if (errorRef.value) {
14616
+ return errorRef.value;
14617
+ }
14618
+ if (!isSocketReady.value || !isRecording.value) {
14619
+ return (_a = labels.value.connecting) != null ? _a : "Connecting...";
14620
+ }
14621
+ return (_b = labels.value.listening) != null ? _b : "Listening...";
14622
+ });
14623
+ watch(transcription, (newValue) => {
14624
+ emit2("transcript-update", newValue);
14625
+ });
14626
+ watch(isRecording, (newVal) => {
14627
+ var _a, _b;
14628
+ if (newVal === true) {
14629
+ (_a = voiceSearchProgressBar.value) == null ? void 0 : _a.startProgressBar();
14630
+ } else {
14631
+ (_b = voiceSearchProgressBar.value) == null ? void 0 : _b.stopProgressBar();
14632
+ }
14633
+ });
14634
+ const handleRecordingButtonClick = () => {
14635
+ var _a;
14636
+ if (isRecording.value) {
14637
+ setTimeout(() => {
14638
+ stopSocketConnection();
14639
+ handleOnStopEvent();
14640
+ }, stopDelay.value);
14641
+ return;
14642
+ }
14643
+ const voiceServiceUrl = getVoiceServiceApiUrl(
14644
+ optionsStore.envOptions.environment,
14645
+ props.options.customVoiceServiceUrl
14646
+ );
14647
+ const socketUrl = `${voiceServiceUrl}?clientId=${clientId.value}&queryKey=${props.options.queryKey}&languageCode=${(_a = props.options.language) != null ? _a : "en-US"}&connectionType=write-first`;
14648
+ initSocket(socketUrl);
14649
+ setTimeout(() => {
14650
+ stopSocketConnection();
14651
+ handleOnStopEvent();
14652
+ }, timesliceLimit.value * timeSliceLength.value);
14653
+ };
14654
+ const handleOnStopEvent = () => {
14655
+ var _a;
14656
+ setTimeout(() => {
14657
+ if (errorRef.value)
14658
+ return;
14659
+ emit2("stop-recognize", transcription.value);
14660
+ }, 1500);
14661
+ (_a = voiceSearchProgressBar.value) == null ? void 0 : _a.stopProgressBar();
14662
+ };
14663
+ onMounted(() => {
14664
+ clientId.value = getSocketClientId();
14665
+ });
14666
+ onBeforeUnmount(() => {
14667
+ clientId.value = null;
14668
+ });
14669
+ const dialogReset = () => {
14670
+ var _a;
14671
+ reset();
14672
+ (_a = voiceSearchProgressBar.value) == null ? void 0 : _a.stopProgressBar();
14673
+ };
14674
+ __expose({
14675
+ handleRecordingButtonClick,
14676
+ reset: dialogReset
14677
+ });
14678
+ return (_ctx, _cache) => {
14679
+ return openBlock(), createElementBlock("div", null, [
14680
+ props.isOpen ? (openBlock(), createElementBlock("div", _hoisted_1$1l, [
14681
+ createBaseVNode("button", {
14682
+ class: "lupa-dialog-box-close-button",
14683
+ onClick: _cache[0] || (_cache[0] = () => emit2("close"))
14684
+ }),
14685
+ createBaseVNode("div", _hoisted_2$W, [
14686
+ createBaseVNode("p", _hoisted_3$F, toDisplayString(description.value), 1),
14687
+ createBaseVNode("div", _hoisted_4$v, [
14688
+ createBaseVNode("button", {
14689
+ class: normalizeClass(["lupa-mic-button", { recording: unref(isRecording) }]),
14690
+ onClick: handleRecordingButtonClick
14691
+ }, null, 2),
14692
+ createVNode(_sfc_main$1z, {
14693
+ ref_key: "voiceSearchProgressBar",
14694
+ ref: voiceSearchProgressBar,
14695
+ class: "lupa-progress-circle",
14696
+ isRecording: unref(isRecording),
14697
+ timesliceLimit: timesliceLimit.value,
14698
+ timeSliceLength: timeSliceLength.value
14699
+ }, null, 8, ["isRecording", "timesliceLimit", "timeSliceLength"])
14700
+ ])
14701
+ ])
14702
+ ])) : createCommentVNode("", true)
14703
+ ]);
14704
+ };
14705
+ }
14706
+ });
14317
14707
  const _hoisted_1$1k = { id: "lupa-search-box-input-container" };
14318
14708
  const _hoisted_2$V = { class: "lupa-input-clear" };
14319
14709
  const _hoisted_3$E = { id: "lupa-search-box-input" };
@@ -14327,6 +14717,7 @@ var __async = (__this, __arguments, generator) => {
14327
14717
  key: 0,
14328
14718
  class: "lupa-close-label"
14329
14719
  };
14720
+ const _hoisted_9$3 = { key: 1 };
14330
14721
  const _sfc_main$1x = /* @__PURE__ */ defineComponent({
14331
14722
  __name: "SearchBoxInput",
14332
14723
  props: {
@@ -14342,6 +14733,8 @@ var __async = (__this, __arguments, generator) => {
14342
14733
  const searchBoxStore = useSearchBoxStore();
14343
14734
  const { query } = storeToRefs(paramStore);
14344
14735
  const mainInput = ref(null);
14736
+ const voiceDialogOverlay = ref(null);
14737
+ const isVoiceDialogOpen = ref(false);
14345
14738
  const emitInputOnFocus = computed(() => {
14346
14739
  var _a;
14347
14740
  return (_a = props.emitInputOnFocus) != null ? _a : true;
@@ -14352,6 +14745,10 @@ var __async = (__this, __arguments, generator) => {
14352
14745
  return (_a = props.suggestedValue) != null ? _a : { value: "", override: false, item: { suggestion: "" } };
14353
14746
  }
14354
14747
  );
14748
+ const isVoiceSearchEnabled = computed(() => {
14749
+ var _a, _b;
14750
+ return (_b = (_a = props.options.voiceSearch) == null ? void 0 : _a.enabled) != null ? _b : false;
14751
+ });
14355
14752
  const labels = computed(() => props.options.labels);
14356
14753
  const input2 = ref("");
14357
14754
  const inputValue = computed({
@@ -14375,6 +14772,12 @@ var __async = (__this, __arguments, generator) => {
14375
14772
  var _a;
14376
14773
  return (_a = labels.value.searchInputAriaLabel) != null ? _a : "Search input";
14377
14774
  });
14775
+ onMounted(() => {
14776
+ document.addEventListener("click", handleClickOutsideVoiceDialogOverlay);
14777
+ });
14778
+ onBeforeUnmount(() => {
14779
+ document.removeEventListener("click", handleClickOutsideVoiceDialogOverlay);
14780
+ });
14378
14781
  watch(suggestedValue, () => {
14379
14782
  if (suggestedValue.value.override) {
14380
14783
  input2.value = suggestedValue.value.item.suggestion;
@@ -14409,6 +14812,37 @@ var __async = (__this, __arguments, generator) => {
14409
14812
  }
14410
14813
  (_a = mainInput == null ? void 0 : mainInput.value) == null ? void 0 : _a.focus();
14411
14814
  };
14815
+ const openVoiceSearchDialog = () => {
14816
+ var _a;
14817
+ isVoiceDialogOpen.value = true;
14818
+ (_a = voiceDialogOverlay.value) == null ? void 0 : _a.handleRecordingButtonClick();
14819
+ };
14820
+ const closeDialog = () => {
14821
+ var _a;
14822
+ isVoiceDialogOpen.value = false;
14823
+ (_a = voiceDialogOverlay.value) == null ? void 0 : _a.reset();
14824
+ };
14825
+ const handleVoiceSearchOutput = (transcription) => {
14826
+ inputValue.value = transcription;
14827
+ handleSubmit();
14828
+ };
14829
+ const stopRecognition = (trascription) => {
14830
+ setTimeout(() => {
14831
+ isVoiceDialogOpen.value = false;
14832
+ handleVoiceSearchOutput(trascription);
14833
+ }, 500);
14834
+ };
14835
+ const handleClickOutsideVoiceDialogOverlay = (event) => {
14836
+ if (event.target.classList.contains("lupa-voice-search-button")) {
14837
+ return;
14838
+ }
14839
+ if (voiceDialogOverlay.value && voiceDialogOverlay.value.$el.contains(event.target)) {
14840
+ return;
14841
+ }
14842
+ if (isVoiceDialogOpen.value) {
14843
+ closeDialog();
14844
+ }
14845
+ };
14412
14846
  __expose({ focus });
14413
14847
  return (_ctx, _cache) => {
14414
14848
  return openBlock(), createElementBlock("div", _hoisted_1$1k, [
@@ -14452,7 +14886,23 @@ var __async = (__this, __arguments, generator) => {
14452
14886
  onClick: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("close"))
14453
14887
  }, [
14454
14888
  labels.value.close ? (openBlock(), createElementBlock("span", _hoisted_8$3, toDisplayString(labels.value.close), 1)) : createCommentVNode("", true)
14455
- ])) : createCommentVNode("", true)
14889
+ ])) : createCommentVNode("", true),
14890
+ isVoiceSearchEnabled.value ? (openBlock(), createElementBlock("div", _hoisted_9$3, [
14891
+ createBaseVNode("button", {
14892
+ onClick: openVoiceSearchDialog,
14893
+ class: "lupa-voice-search-button"
14894
+ })
14895
+ ])) : createCommentVNode("", true),
14896
+ isVoiceSearchEnabled.value ? (openBlock(), createBlock(_sfc_main$1y, {
14897
+ key: 2,
14898
+ ref_key: "voiceDialogOverlay",
14899
+ ref: voiceDialogOverlay,
14900
+ isOpen: isVoiceDialogOpen.value,
14901
+ options: props.options.voiceSearch,
14902
+ onClose: closeDialog,
14903
+ onTranscriptUpdate: handleVoiceSearchOutput,
14904
+ onStopRecognize: stopRecognition
14905
+ }, null, 8, ["isOpen", "options"])) : createCommentVNode("", true)
14456
14906
  ]);
14457
14907
  };
14458
14908
  }
@@ -23878,6 +24328,15 @@ and ensure you are accounting for this risk.
23878
24328
  return false;
23879
24329
  }
23880
24330
  };
24331
+ function shouldDisplay(displayOpt, doc2) {
24332
+ if (!displayOpt)
24333
+ return true;
24334
+ if (typeof displayOpt === "function") {
24335
+ return displayOpt(doc2);
24336
+ }
24337
+ const rules = Array.isArray(displayOpt) ? displayOpt : [displayOpt];
24338
+ return rules.every((rule2) => processDisplayCondition(rule2, doc2));
24339
+ }
23881
24340
  const checkHasFullImageUrl = (imageUrl) => typeof imageUrl === "string" && (imageUrl.indexOf("http://") === 0 || imageUrl.indexOf("https://") === 0);
23882
24341
  const computeImageUrl = (imageUrl, rootImageUrl) => {
23883
24342
  const mainUrl = Array.isArray(imageUrl) ? imageUrl[0] : imageUrl;
@@ -24694,7 +25153,7 @@ and ensure you are accounting for this risk.
24694
25153
  if (!element.display) {
24695
25154
  return true;
24696
25155
  }
24697
- return typeof element.display === "function" ? element.display(item) : processDisplayCondition(element.display, item);
25156
+ return shouldDisplay(element.display, item);
24698
25157
  });
24699
25158
  const enhancedItem = computed(() => {
24700
25159
  var _a, _b, _c, _d;
@@ -25017,7 +25476,7 @@ and ensure you are accounting for this risk.
25017
25476
  if (!element.display) {
25018
25477
  return true;
25019
25478
  }
25020
- return typeof element.display === "function" ? element.display(item) : processDisplayCondition(element.display, item);
25479
+ return shouldDisplay(element.display, item);
25021
25480
  };
25022
25481
  const badges = computed(() => {
25023
25482
  if (!props.options.elements) {
@@ -25095,10 +25554,6 @@ and ensure you are accounting for this risk.
25095
25554
  const badgeOptions = computed(() => {
25096
25555
  return __spreadProps2(__spreadValues2({}, props.panelOptions.badges), { product: props.item });
25097
25556
  });
25098
- const imageElements = computed(() => {
25099
- var _a, _b;
25100
- return (_b = (_a = props.panelOptions.elements) == null ? void 0 : _a.filter((e2) => e2.type === DocumentElementType.IMAGE)) != null ? _b : [];
25101
- });
25102
25557
  const mainImageElement = computed(() => {
25103
25558
  return imageElements.value[0];
25104
25559
  });
@@ -25118,12 +25573,6 @@ and ensure you are accounting for this risk.
25118
25573
  minWidth: widthOverride.value ? `${widthOverride.value + 10}px` : void 0
25119
25574
  } : {};
25120
25575
  });
25121
- const detailElements = computed(() => {
25122
- var _a, _b;
25123
- return (_b = (_a = props.panelOptions.elements) == null ? void 0 : _a.filter(
25124
- (e2) => e2.type !== DocumentElementType.IMAGE && e2.type !== DocumentElementType.ADDTOCART
25125
- )) != null ? _b : [];
25126
- });
25127
25576
  const addToCartElement = computed(() => {
25128
25577
  var _a;
25129
25578
  return (_a = props.panelOptions.elements) == null ? void 0 : _a.find((e2) => e2.type === DocumentElementType.ADDTOCART);
@@ -25142,12 +25591,42 @@ and ensure you are accounting for this risk.
25142
25591
  onMounted(() => {
25143
25592
  checkIfIsInStock();
25144
25593
  });
25145
- const processIsInStock = () => {
25146
- return typeof props.panelOptions.isInStock === "function" ? props.panelOptions.isInStock(props.item) : processDisplayCondition(props.panelOptions.isInStock, props.item);
25147
- };
25594
+ const processIsInStock = computed(() => {
25595
+ const raw = props.panelOptions.isInStock;
25596
+ if (!raw)
25597
+ return true;
25598
+ const rules = Array.isArray(raw) ? raw : [raw];
25599
+ return rules.every((rule2) => shouldDisplay(rule2, props.item));
25600
+ });
25148
25601
  const checkIfIsInStock = () => __async2(this, null, function* () {
25149
- isInStock.value = props.panelOptions.isInStock ? processIsInStock() : true;
25602
+ isInStock.value = props.panelOptions.isInStock ? processIsInStock.value : true;
25150
25603
  });
25604
+ const imageElements = computed(
25605
+ () => {
25606
+ var _a, _b;
25607
+ return (_b = (_a = props.panelOptions.elements) == null ? void 0 : _a.filter((e2) => e2.type === DocumentElementType.IMAGE && !e2.group)) != null ? _b : [];
25608
+ }
25609
+ );
25610
+ const detailElements = computed(
25611
+ () => {
25612
+ var _a, _b;
25613
+ return (_b = (_a = props.panelOptions.elements) == null ? void 0 : _a.filter(
25614
+ (e2) => e2.type !== DocumentElementType.IMAGE && e2.type !== DocumentElementType.ADDTOCART && !e2.group
25615
+ )) != null ? _b : [];
25616
+ }
25617
+ );
25618
+ const elementGroups = computed(
25619
+ () => {
25620
+ var _a;
25621
+ return Array.from(
25622
+ new Set((_a = props.panelOptions.elements) == null ? void 0 : _a.map((e2) => e2.group).filter((g) => Boolean(g)))
25623
+ );
25624
+ }
25625
+ );
25626
+ function getGroupElements(group) {
25627
+ var _a, _b;
25628
+ return (_b = (_a = props.panelOptions.elements) == null ? void 0 : _a.filter((e2) => e2.group === group)) != null ? _b : [];
25629
+ }
25151
25630
  return (_ctx, _cache) => {
25152
25631
  return openBlock(), createElementBlock("a", mergeProps({
25153
25632
  class: ["lupa-search-box-product", { "lupa-search-box-product-highlighted": _ctx.highlighted }],
@@ -25163,9 +25642,9 @@ and ensure you are accounting for this risk.
25163
25642
  (openBlock(true), createElementBlock(Fragment, null, renderList(imageElements.value, (element) => {
25164
25643
  return openBlock(), createBlock(_sfc_main$1g, {
25165
25644
  class: "lupa-search-box-product-element",
25645
+ key: element.key,
25166
25646
  item: _ctx.item,
25167
25647
  element,
25168
- key: element.key,
25169
25648
  labels: _ctx.labels,
25170
25649
  link: link.value
25171
25650
  }, null, 8, ["item", "element", "labels", "link"]);
@@ -25175,14 +25654,14 @@ and ensure you are accounting for this risk.
25175
25654
  (openBlock(true), createElementBlock(Fragment, null, renderList(detailElements.value, (element) => {
25176
25655
  var _a;
25177
25656
  return openBlock(), createBlock(_sfc_main$1g, {
25178
- key: element.key,
25179
25657
  class: "lupa-search-box-product-element",
25658
+ key: element.key,
25180
25659
  item: _ctx.item,
25181
25660
  element,
25182
25661
  labels: _ctx.labels,
25183
25662
  link: link.value
25184
25663
  }, createSlots({ _: 2 }, [
25185
- badgeOptions.value && ((_a = badgeOptions.value) == null ? void 0 : _a.anchorElementKey) === element.key ? {
25664
+ ((_a = badgeOptions.value) == null ? void 0 : _a.anchorElementKey) === element.key ? {
25186
25665
  name: "badges",
25187
25666
  fn: withCtx(() => [
25188
25667
  createVNode(_sfc_main$19, {
@@ -25195,6 +25674,24 @@ and ensure you are accounting for this risk.
25195
25674
  ]), 1032, ["item", "element", "labels", "link"]);
25196
25675
  }), 128))
25197
25676
  ]),
25677
+ (openBlock(true), createElementBlock(Fragment, null, renderList(elementGroups.value, (group) => {
25678
+ return openBlock(), createElementBlock("div", {
25679
+ key: group,
25680
+ class: normalizeClass(`lupa-search-box-group-${group}`)
25681
+ }, [
25682
+ (openBlock(true), createElementBlock(Fragment, null, renderList(getGroupElements(group), (element) => {
25683
+ return openBlock(), createBlock(_sfc_main$1g, {
25684
+ class: "lupa-search-box-product-element",
25685
+ key: element.key,
25686
+ item: _ctx.item,
25687
+ element,
25688
+ labels: _ctx.labels,
25689
+ link: link.value,
25690
+ isInStock: isInStock.value
25691
+ }, null, 8, ["item", "element", "labels", "link", "isInStock"]);
25692
+ }), 128))
25693
+ ], 2);
25694
+ }), 128)),
25198
25695
  addToCartElement.value ? (openBlock(), createElementBlock("div", _hoisted_3$z, [
25199
25696
  createVNode(_sfc_main$1g, {
25200
25697
  class: "lupa-search-box-product-element",
@@ -25991,7 +26488,8 @@ and ensure you are accounting for this risk.
25991
26488
  "labels",
25992
26489
  "links",
25993
26490
  "inputAttributes",
25994
- "showSubmitButton"
26491
+ "showSubmitButton",
26492
+ "voiceSearch"
25995
26493
  ])
25996
26494
  );
25997
26495
  const panelOptions = computed(
@@ -26575,22 +27073,42 @@ and ensure you are accounting for this risk.
26575
27073
  emits: ["remove"],
26576
27074
  setup(__props, { emit: emit2 }) {
26577
27075
  const props = __props;
26578
- const facetKeyClass = computed(() => {
26579
- return `lupa-facet-active-filter-${props.filter.key}`;
27076
+ const facetKeyClass = computed(() => `lupa-facet-active-filter-${props.filter.key}`);
27077
+ const { searchResultOptions } = storeToRefs(useOptionsStore());
27078
+ const units = computed(() => {
27079
+ var _a, _b, _c, _d, _e;
27080
+ return (_e = (_d = (_c = (_b = (_a = searchResultOptions.value) == null ? void 0 : _a.filters) == null ? void 0 : _b.facets) == null ? void 0 : _c.stats) == null ? void 0 : _d.units) != null ? _e : {};
26580
27081
  });
26581
- const handleClick = () => {
27082
+ function handleClick() {
26582
27083
  emit2("remove", { filter: props.filter });
26583
- };
27084
+ }
27085
+ function formatFilterValue(filter2) {
27086
+ const unit = units.value[filter2.key] || "";
27087
+ let min, max;
27088
+ if (Array.isArray(filter2.value)) {
27089
+ [min, max] = filter2.value.map(String);
27090
+ } else if (typeof filter2.value === "string" && filter2.value.includes("-")) {
27091
+ const parts = filter2.value.split("-").map((s) => s.trim());
27092
+ if (parts.length === 2)
27093
+ [min, max] = parts;
27094
+ }
27095
+ if (min != null && max != null) {
27096
+ return `${min} ${unit} – ${max} ${unit}`;
27097
+ }
27098
+ return `${filter2.value} ${unit}`.trim();
27099
+ }
26584
27100
  return (_ctx, _cache) => {
26585
27101
  return openBlock(), createElementBlock("div", {
26586
- class: normalizeClass(["lupa-search-result-filter-value", { [facetKeyClass.value]: true }])
27102
+ class: normalizeClass(["lupa-search-result-filter-value", [facetKeyClass.value]]),
27103
+ "data-cy": "lupa-current-filter-item"
26587
27104
  }, [
26588
27105
  createBaseVNode("div", {
26589
27106
  class: "lupa-current-filter-action",
26590
- onClick: handleClick
26591
- }, ""),
27107
+ onClick: handleClick,
27108
+ "aria-label": "Remove filter"
27109
+ }, " ⨉ "),
26592
27110
  createBaseVNode("div", _hoisted_1$U, toDisplayString(_ctx.filter.label) + ": ", 1),
26593
- createBaseVNode("div", _hoisted_2$F, toDisplayString(_ctx.filter.value), 1)
27111
+ createBaseVNode("div", _hoisted_2$F, toDisplayString(formatFilterValue(props.filter)), 1)
26594
27112
  ], 2);
26595
27113
  };
26596
27114
  }
@@ -26612,6 +27130,14 @@ and ensure you are accounting for this risk.
26612
27130
  expandable: { type: Boolean }
26613
27131
  },
26614
27132
  setup(__props) {
27133
+ const optionsStore = useOptionsStore();
27134
+ const { searchResultOptions } = storeToRefs(optionsStore);
27135
+ const units = computed(
27136
+ () => {
27137
+ var _a, _b, _c, _d, _e;
27138
+ return (_e = (_d = (_c = (_b = (_a = searchResultOptions == null ? void 0 : searchResultOptions.value) == null ? void 0 : _a.filters) == null ? void 0 : _b.facets) == null ? void 0 : _c.stats) == null ? void 0 : _d.units) != null ? _e : {};
27139
+ }
27140
+ );
26615
27141
  const isOpen = ref(false);
26616
27142
  const paramsStore = useParamsStore();
26617
27143
  const optionStore = useOptionsStore();
@@ -26693,8 +27219,9 @@ and ensure you are accounting for this risk.
26693
27219
  return openBlock(), createBlock(_sfc_main$_, {
26694
27220
  key: filter2.key + "_" + filter2.value,
26695
27221
  filter: filter2,
27222
+ units: units.value,
26696
27223
  onRemove: handleRemove
26697
- }, null, 8, ["filter"]);
27224
+ }, null, 8, ["filter", "units"]);
26698
27225
  }), 128))
26699
27226
  ]),
26700
27227
  createBaseVNode("div", {
@@ -28018,15 +28545,17 @@ and ensure you are accounting for this risk.
28018
28545
  const _hoisted_5$d = { class: "lupa-stats-from" };
28019
28546
  const _hoisted_6$7 = ["max", "min", "pattern", "aria-label"];
28020
28547
  const _hoisted_7$5 = { key: 0 };
28021
- const _hoisted_8$1 = /* @__PURE__ */ createBaseVNode("div", { class: "lupa-stats-separator" }, null, -1);
28022
- const _hoisted_9$1 = {
28548
+ const _hoisted_8$1 = { key: 1 };
28549
+ const _hoisted_9$1 = /* @__PURE__ */ createBaseVNode("div", { class: "lupa-stats-separator" }, null, -1);
28550
+ const _hoisted_10 = {
28023
28551
  key: 0,
28024
28552
  class: "lupa-stats-range-label"
28025
28553
  };
28026
- const _hoisted_10 = { class: "lupa-stats-to" };
28027
- const _hoisted_11 = ["max", "min", "pattern", "aria-label"];
28028
- const _hoisted_12 = { key: 0 };
28029
- const _hoisted_13 = {
28554
+ const _hoisted_11 = { class: "lupa-stats-to" };
28555
+ const _hoisted_12 = ["max", "min", "pattern", "aria-label"];
28556
+ const _hoisted_13 = { key: 0 };
28557
+ const _hoisted_14 = { key: 1 };
28558
+ const _hoisted_15 = {
28030
28559
  key: 2,
28031
28560
  class: "lupa-stats-slider-wrapper"
28032
28561
  };
@@ -28093,7 +28622,7 @@ and ensure you are accounting for this risk.
28093
28622
  if (!value || value > facetMax.value) {
28094
28623
  return;
28095
28624
  }
28096
- innerSliderRange.value = [value, sliderRange.value[1]];
28625
+ innerSliderRange.value = [sliderRange.value[1], value];
28097
28626
  handleInputChange();
28098
28627
  }
28099
28628
  });
@@ -28149,7 +28678,18 @@ and ensure you are accounting for this risk.
28149
28678
  });
28150
28679
  const statsSummary = computed(() => {
28151
28680
  const [min, max] = sliderRange.value;
28152
- return isPrice.value ? formatPriceSummary([min, max], currency.value, separator.value, currencyTemplate.value) : formatRange({ gte: min, lte: max });
28681
+ if (isPrice.value) {
28682
+ return formatPriceSummary(
28683
+ [min, max],
28684
+ currency.value,
28685
+ separator.value,
28686
+ currencyTemplate.value
28687
+ );
28688
+ }
28689
+ if (unit.value) {
28690
+ return `${min} ${unit.value} - ${max} ${unit.value}`;
28691
+ }
28692
+ return formatRange({ gte: min, lte: max });
28153
28693
  });
28154
28694
  const separator = computed(() => {
28155
28695
  var _a, _b, _c;
@@ -28210,6 +28750,12 @@ and ensure you are accounting for this risk.
28210
28750
  const handleDragging = (value) => {
28211
28751
  innerSliderRange.value = value;
28212
28752
  };
28753
+ const unit = computed(
28754
+ () => {
28755
+ var _a, _b, _c, _d, _e;
28756
+ return (_e = (_d = (_a = props.options.stats) == null ? void 0 : _a.units) == null ? void 0 : _d[(_c = (_b = props.facet) == null ? void 0 : _b.key) != null ? _c : ""]) != null ? _e : "";
28757
+ }
28758
+ );
28213
28759
  return (_ctx, _cache) => {
28214
28760
  return openBlock(), createElementBlock("div", _hoisted_1$P, [
28215
28761
  !isInputVisible.value ? (openBlock(), createElementBlock("div", _hoisted_2$B, toDisplayString(statsSummary.value), 1)) : (openBlock(), createElementBlock("div", _hoisted_3$s, [
@@ -28232,13 +28778,14 @@ and ensure you are accounting for this risk.
28232
28778
  { lazy: true }
28233
28779
  ]
28234
28780
  ]),
28235
- isPrice.value ? (openBlock(), createElementBlock("span", _hoisted_7$5, toDisplayString(currency.value), 1)) : createCommentVNode("", true)
28781
+ isPrice.value ? (openBlock(), createElementBlock("span", _hoisted_7$5, toDisplayString(currency.value), 1)) : createCommentVNode("", true),
28782
+ unit.value ? (openBlock(), createElementBlock("span", _hoisted_8$1, toDisplayString(unit.value), 1)) : createCommentVNode("", true)
28236
28783
  ])
28237
28784
  ]),
28238
- _hoisted_8$1,
28785
+ _hoisted_9$1,
28239
28786
  createBaseVNode("div", null, [
28240
- rangeLabelTo.value ? (openBlock(), createElementBlock("div", _hoisted_9$1, toDisplayString(rangeLabelTo.value), 1)) : createCommentVNode("", true),
28241
- createBaseVNode("div", _hoisted_10, [
28787
+ rangeLabelTo.value ? (openBlock(), createElementBlock("div", _hoisted_10, toDisplayString(rangeLabelTo.value), 1)) : createCommentVNode("", true),
28788
+ createBaseVNode("div", _hoisted_11, [
28242
28789
  withDirectives(createBaseVNode("input", {
28243
28790
  "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => toValue.value = $event),
28244
28791
  type: "text",
@@ -28247,7 +28794,7 @@ and ensure you are accounting for this risk.
28247
28794
  min: facetMin.value,
28248
28795
  pattern: sliderInputFormat.value,
28249
28796
  "aria-label": ariaLabelTo.value
28250
- }, null, 8, _hoisted_11), [
28797
+ }, null, 8, _hoisted_12), [
28251
28798
  [
28252
28799
  vModelText,
28253
28800
  toValue.value,
@@ -28255,11 +28802,12 @@ and ensure you are accounting for this risk.
28255
28802
  { lazy: true }
28256
28803
  ]
28257
28804
  ]),
28258
- isPrice.value ? (openBlock(), createElementBlock("span", _hoisted_12, toDisplayString(currency.value), 1)) : createCommentVNode("", true)
28805
+ isPrice.value ? (openBlock(), createElementBlock("span", _hoisted_13, toDisplayString(currency.value), 1)) : createCommentVNode("", true),
28806
+ unit.value ? (openBlock(), createElementBlock("span", _hoisted_14, toDisplayString(unit.value), 1)) : createCommentVNode("", true)
28259
28807
  ])
28260
28808
  ])
28261
28809
  ])),
28262
- isSliderVisible.value ? (openBlock(), createElementBlock("div", _hoisted_13, [
28810
+ isSliderVisible.value ? (openBlock(), createElementBlock("div", _hoisted_15, [
28263
28811
  createVNode(unref(m), {
28264
28812
  class: "slider",
28265
28813
  tooltips: false,
@@ -30140,14 +30688,9 @@ and ensure you are accounting for this risk.
30140
30688
  const enhancementData = (_d = (_c = dynamicDataIdMap.value) == null ? void 0 : _c[(_b = props.item) == null ? void 0 : _b.id]) != null ? _d : {};
30141
30689
  return __spreadValues2(__spreadValues2({}, props.item), enhancementData);
30142
30690
  });
30143
- const displayElement = computed(() => {
30144
- const element = props.element;
30145
- const item = enhancedItem.value;
30146
- if (!element.display) {
30147
- return true;
30148
- }
30149
- return typeof element.display === "function" ? element.display(item) : processDisplayCondition(element.display, item);
30150
- });
30691
+ const displayElement = computed(
30692
+ () => shouldDisplay(props.element.display, enhancedItem.value)
30693
+ );
30151
30694
  const dynamicAttributes = computed(() => {
30152
30695
  return getDynamicAttributes(props.element.dynamicAttributes, enhancedItem.value);
30153
30696
  });
@@ -30275,15 +30818,21 @@ and ensure you are accounting for this risk.
30275
30818
  var _a, _b;
30276
30819
  return (_b = (_a = props.options.elements) == null ? void 0 : _a.filter((e2) => e2.group === group)) != null ? _b : [];
30277
30820
  };
30278
- const processIsInStock = () => {
30279
- return typeof props.options.isInStock === "function" ? props.options.isInStock(props.product) : processDisplayCondition(props.options.isInStock, props.product);
30821
+ const shouldShowInStock = () => {
30822
+ const raw = props.options.isInStock;
30823
+ if (!raw)
30824
+ return true;
30825
+ const rules = Array.isArray(raw) ? raw : [raw];
30826
+ return rules.every(
30827
+ (rule2) => typeof rule2 === "function" ? rule2(props.product) : processDisplayCondition(rule2, props.product)
30828
+ );
30829
+ };
30830
+ const checkIfIsInStock = () => {
30831
+ isInStock.value = shouldShowInStock();
30280
30832
  };
30281
30833
  onMounted(() => {
30282
30834
  checkIfIsInStock();
30283
30835
  });
30284
- const checkIfIsInStock = () => __async2(this, null, function* () {
30285
- isInStock.value = props.options.isInStock ? yield processIsInStock() : true;
30286
- });
30287
30836
  const handleClick = () => {
30288
30837
  var _a, _b, _c, _d;
30289
30838
  const event = {
@@ -33162,21 +33711,6 @@ and ensure you are accounting for this risk.
33162
33711
  };
33163
33712
  }
33164
33713
  });
33165
- const Env = {
33166
- production: "https://api.lupasearch.com/v1/",
33167
- staging: "https://api.staging.lupasearch.com/v1/"
33168
- };
33169
- const DEFAULT_REQUEST_CONFIG = {
33170
- method: "POST",
33171
- headers: { "Content-Type": "application/json" }
33172
- };
33173
- const DEFAULT_HEADERS = DEFAULT_REQUEST_CONFIG.headers;
33174
- const getApiUrl = (environment, customBaseUrl) => {
33175
- if (customBaseUrl) {
33176
- return customBaseUrl;
33177
- }
33178
- return Env[environment] || Env["production"];
33179
- };
33180
33714
  const suggestSearchChatPhrases = (options, request, chatSettings) => __async2(void 0, null, function* () {
33181
33715
  var _a, _b, _c;
33182
33716
  const { environment, customBaseUrl } = options;
@@ -33683,7 +34217,7 @@ and ensure you are accounting for this risk.
33683
34217
  key: 0,
33684
34218
  class: "lupasearch-chat-content"
33685
34219
  };
33686
- const _sfc_main$1y = /* @__PURE__ */ defineComponent({
34220
+ const _sfc_main$1A = /* @__PURE__ */ defineComponent({
33687
34221
  __name: "ChatContainer",
33688
34222
  props: {
33689
34223
  options: {}
@@ -40337,7 +40871,7 @@ and ensure you are accounting for this risk.
40337
40871
  const instance = createVue(
40338
40872
  options.displayOptions.containerSelector,
40339
40873
  mountOptions == null ? void 0 : mountOptions.mountingBehavior,
40340
- _sfc_main$1y,
40874
+ _sfc_main$1A,
40341
40875
  {
40342
40876
  options
40343
40877
  }