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