@seekora-ai/ui-sdk-react 0.2.0 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/FederatedDropdown.d.ts +6 -0
- package/dist/components/FederatedDropdown.d.ts.map +1 -1
- package/dist/components/FederatedDropdown.js +4 -2
- package/dist/components/SearchBarWithSuggestions.d.ts +4 -0
- package/dist/components/SearchBarWithSuggestions.d.ts.map +1 -1
- package/dist/components/SearchBarWithSuggestions.js +2 -2
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +24 -58
- package/dist/docsearch/components/DocSearch.d.ts +4 -0
- package/dist/docsearch/components/DocSearch.d.ts.map +1 -0
- package/dist/docsearch/components/DocSearch.js +91 -0
- package/dist/docsearch/components/DocSearchButton.d.ts +4 -0
- package/dist/docsearch/components/DocSearchButton.d.ts.map +1 -0
- package/dist/docsearch/components/DocSearchButton.js +12 -0
- package/dist/docsearch/components/Footer.d.ts +8 -0
- package/dist/docsearch/components/Footer.d.ts.map +1 -0
- package/dist/docsearch/components/Footer.js +23 -0
- package/dist/docsearch/components/Highlight.d.ts +9 -0
- package/dist/docsearch/components/Highlight.d.ts.map +1 -0
- package/dist/docsearch/components/Highlight.js +48 -0
- package/dist/docsearch/components/Hit.d.ts +15 -0
- package/dist/docsearch/components/Hit.d.ts.map +1 -0
- package/dist/docsearch/components/Hit.js +96 -0
- package/dist/docsearch/components/Modal.d.ts +9 -0
- package/dist/docsearch/components/Modal.d.ts.map +1 -0
- package/dist/docsearch/components/Modal.js +54 -0
- package/dist/docsearch/components/Results.d.ts +23 -0
- package/dist/docsearch/components/Results.d.ts.map +1 -0
- package/dist/docsearch/components/Results.js +145 -0
- package/dist/docsearch/components/SearchBox.d.ts +12 -0
- package/dist/docsearch/components/SearchBox.d.ts.map +1 -0
- package/dist/docsearch/components/SearchBox.js +18 -0
- package/dist/docsearch/hooks/useDocSearch.d.ts +33 -0
- package/dist/docsearch/hooks/useDocSearch.d.ts.map +1 -0
- package/dist/docsearch/hooks/useDocSearch.js +211 -0
- package/dist/docsearch/hooks/useKeyboard.d.ts +17 -0
- package/dist/docsearch/hooks/useKeyboard.d.ts.map +1 -0
- package/dist/docsearch/hooks/useKeyboard.js +71 -0
- package/dist/docsearch/hooks/useSeekoraSearch.d.ts +27 -0
- package/dist/docsearch/hooks/useSeekoraSearch.d.ts.map +1 -0
- package/dist/docsearch/hooks/useSeekoraSearch.js +207 -0
- package/dist/docsearch/index.d.ts +13 -0
- package/dist/docsearch/index.d.ts.map +1 -0
- package/dist/docsearch/index.js +11 -0
- package/dist/docsearch/types.d.ts +172 -0
- package/dist/docsearch/types.d.ts.map +1 -0
- package/dist/docsearch/types.js +4 -0
- package/dist/docsearch.css +237 -0
- package/dist/hooks/useAnalytics.d.ts +8 -4
- package/dist/hooks/useAnalytics.d.ts.map +1 -1
- package/dist/hooks/useAnalytics.js +14 -9
- package/dist/hooks/useQuerySuggestionsEnhanced.js +1 -1
- package/dist/hooks/useSuggestionsAnalytics.d.ts +3 -1
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
- package/dist/hooks/useSuggestionsAnalytics.js +11 -9
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +271 -7
- package/dist/src/index.esm.js +1705 -84
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +1712 -83
- package/dist/src/index.js.map +1 -1
- package/package.json +9 -6
- package/src/docsearch/docsearch.css +237 -0
package/dist/src/index.esm.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo, useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
|
1
|
+
import React, { createContext, useContext, useMemo, useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useReducer } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { SeekoraClient } from '@seekora-ai/search-sdk';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Field Mapping Utilities
|
|
@@ -565,6 +567,688 @@ class SearchStateManager {
|
|
|
565
567
|
}
|
|
566
568
|
}
|
|
567
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Seekora Device Fingerprinting Module
|
|
572
|
+
*
|
|
573
|
+
* A lightweight, dependency-free device fingerprinting solution that collects
|
|
574
|
+
* various browser and device signals to generate a unique visitor identifier.
|
|
575
|
+
*/
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// MurmurHash3 Implementation (32-bit)
|
|
578
|
+
// ============================================================================
|
|
579
|
+
function murmurHash3(str, seed = 0) {
|
|
580
|
+
const remainder = str.length & 3; // str.length % 4
|
|
581
|
+
const bytes = str.length - remainder;
|
|
582
|
+
let h1 = seed;
|
|
583
|
+
const c1 = 0xcc9e2d51;
|
|
584
|
+
const c2 = 0x1b873593;
|
|
585
|
+
let i = 0;
|
|
586
|
+
let k1;
|
|
587
|
+
while (i < bytes) {
|
|
588
|
+
k1 =
|
|
589
|
+
(str.charCodeAt(i) & 0xff) |
|
|
590
|
+
((str.charCodeAt(++i) & 0xff) << 8) |
|
|
591
|
+
((str.charCodeAt(++i) & 0xff) << 16) |
|
|
592
|
+
((str.charCodeAt(++i) & 0xff) << 24);
|
|
593
|
+
++i;
|
|
594
|
+
k1 = Math.imul(k1, c1);
|
|
595
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
596
|
+
k1 = Math.imul(k1, c2);
|
|
597
|
+
h1 ^= k1;
|
|
598
|
+
h1 = (h1 << 13) | (h1 >>> 19);
|
|
599
|
+
h1 = Math.imul(h1, 5) + 0xe6546b64;
|
|
600
|
+
}
|
|
601
|
+
k1 = 0;
|
|
602
|
+
switch (remainder) {
|
|
603
|
+
case 3:
|
|
604
|
+
k1 ^= (str.charCodeAt(i + 2) & 0xff) << 16;
|
|
605
|
+
// fallthrough
|
|
606
|
+
case 2:
|
|
607
|
+
k1 ^= (str.charCodeAt(i + 1) & 0xff) << 8;
|
|
608
|
+
// fallthrough
|
|
609
|
+
case 1:
|
|
610
|
+
k1 ^= str.charCodeAt(i) & 0xff;
|
|
611
|
+
k1 = Math.imul(k1, c1);
|
|
612
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
613
|
+
k1 = Math.imul(k1, c2);
|
|
614
|
+
h1 ^= k1;
|
|
615
|
+
}
|
|
616
|
+
h1 ^= str.length;
|
|
617
|
+
h1 ^= h1 >>> 16;
|
|
618
|
+
h1 = Math.imul(h1, 0x85ebca6b);
|
|
619
|
+
h1 ^= h1 >>> 13;
|
|
620
|
+
h1 = Math.imul(h1, 0xc2b2ae35);
|
|
621
|
+
h1 ^= h1 >>> 16;
|
|
622
|
+
return h1 >>> 0;
|
|
623
|
+
}
|
|
624
|
+
function murmurHash3Hex(str, seed = 0) {
|
|
625
|
+
return murmurHash3(str, seed).toString(16).padStart(8, '0');
|
|
626
|
+
}
|
|
627
|
+
// ============================================================================
|
|
628
|
+
// Default Configuration
|
|
629
|
+
// ============================================================================
|
|
630
|
+
const DEFAULT_CONFIG = {
|
|
631
|
+
enableCanvas: true,
|
|
632
|
+
enableWebGL: true,
|
|
633
|
+
enableAudio: true,
|
|
634
|
+
enableFonts: true,
|
|
635
|
+
enableHardware: true,
|
|
636
|
+
timeout: 5000,
|
|
637
|
+
};
|
|
638
|
+
// ============================================================================
|
|
639
|
+
// Font List for Detection
|
|
640
|
+
// ============================================================================
|
|
641
|
+
const FONTS_TO_CHECK = [
|
|
642
|
+
// Windows fonts
|
|
643
|
+
'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS',
|
|
644
|
+
'Consolas', 'Courier New', 'Georgia', 'Impact', 'Lucida Console',
|
|
645
|
+
'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Palatino Linotype',
|
|
646
|
+
'Segoe UI', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana',
|
|
647
|
+
// macOS fonts
|
|
648
|
+
'American Typewriter', 'Andale Mono', 'Apple Chancery', 'Apple Color Emoji',
|
|
649
|
+
'Apple SD Gothic Neo', 'Arial Hebrew', 'Avenir', 'Baskerville',
|
|
650
|
+
'Big Caslon', 'Brush Script MT', 'Chalkboard', 'Cochin', 'Copperplate',
|
|
651
|
+
'Didot', 'Futura', 'Geneva', 'Gill Sans', 'Helvetica', 'Helvetica Neue',
|
|
652
|
+
'Herculanum', 'Hoefler Text', 'Lucida Grande', 'Marker Felt', 'Menlo',
|
|
653
|
+
'Monaco', 'Noteworthy', 'Optima', 'Papyrus', 'Phosphate', 'Rockwell',
|
|
654
|
+
'San Francisco', 'Savoye LET', 'SignPainter', 'Skia', 'Snell Roundhand',
|
|
655
|
+
'Zapfino',
|
|
656
|
+
// Linux fonts
|
|
657
|
+
'Cantarell', 'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif',
|
|
658
|
+
'Droid Sans', 'Droid Sans Mono', 'Droid Serif', 'FreeMono', 'FreeSans',
|
|
659
|
+
'FreeSerif', 'Liberation Mono', 'Liberation Sans', 'Liberation Serif',
|
|
660
|
+
'Noto Sans', 'Noto Serif', 'Open Sans', 'Roboto', 'Ubuntu', 'Ubuntu Mono',
|
|
661
|
+
// Common web fonts
|
|
662
|
+
'Lato', 'Montserrat', 'Oswald', 'Raleway', 'Source Sans Pro',
|
|
663
|
+
'PT Sans', 'Merriweather', 'Nunito', 'Playfair Display', 'Poppins',
|
|
664
|
+
];
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// Fingerprint Class
|
|
667
|
+
// ============================================================================
|
|
668
|
+
class Fingerprint {
|
|
669
|
+
constructor(config) {
|
|
670
|
+
this.cachedResult = null;
|
|
671
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Get the complete fingerprint result
|
|
675
|
+
*/
|
|
676
|
+
async get() {
|
|
677
|
+
if (this.cachedResult) {
|
|
678
|
+
return this.cachedResult;
|
|
679
|
+
}
|
|
680
|
+
const components = await this.collectComponents();
|
|
681
|
+
const confidence = this.calculateConfidence(components);
|
|
682
|
+
const visitorId = this.generateVisitorId(components);
|
|
683
|
+
this.cachedResult = {
|
|
684
|
+
visitorId,
|
|
685
|
+
confidence,
|
|
686
|
+
components,
|
|
687
|
+
};
|
|
688
|
+
return this.cachedResult;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Clear the cached result to force re-fingerprinting
|
|
692
|
+
*/
|
|
693
|
+
clearCache() {
|
|
694
|
+
this.cachedResult = null;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Collect all fingerprint components
|
|
698
|
+
*/
|
|
699
|
+
async collectComponents() {
|
|
700
|
+
const [canvas, webgl, audio, adBlockerDetected, incognitoDetected,] = await Promise.all([
|
|
701
|
+
this.config.enableCanvas ? this.getCanvasFingerprint() : { hash: '' },
|
|
702
|
+
this.config.enableWebGL ? this.getWebGLFingerprint() : { renderer: '', vendor: '', hash: '' },
|
|
703
|
+
this.config.enableAudio ? this.getAudioFingerprint() : { hash: '' },
|
|
704
|
+
this.detectAdBlocker(),
|
|
705
|
+
this.detectIncognito(),
|
|
706
|
+
]);
|
|
707
|
+
const fonts = this.config.enableFonts ? this.detectFonts() : [];
|
|
708
|
+
const hardware = this.config.enableHardware ? this.getHardwareInfo() : {
|
|
709
|
+
concurrency: 0,
|
|
710
|
+
deviceMemory: 0,
|
|
711
|
+
maxTouchPoints: 0,
|
|
712
|
+
platform: '',
|
|
713
|
+
};
|
|
714
|
+
const screen = this.getScreenInfo();
|
|
715
|
+
const browser = this.getBrowserInfo();
|
|
716
|
+
return {
|
|
717
|
+
canvas,
|
|
718
|
+
webgl,
|
|
719
|
+
audio,
|
|
720
|
+
fonts,
|
|
721
|
+
hardware,
|
|
722
|
+
screen,
|
|
723
|
+
browser,
|
|
724
|
+
adBlockerDetected,
|
|
725
|
+
incognitoDetected,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Canvas fingerprinting - draws various shapes and text, then hashes the result
|
|
730
|
+
*/
|
|
731
|
+
async getCanvasFingerprint() {
|
|
732
|
+
try {
|
|
733
|
+
const canvas = document.createElement('canvas');
|
|
734
|
+
canvas.width = 256;
|
|
735
|
+
canvas.height = 128;
|
|
736
|
+
const ctx = canvas.getContext('2d');
|
|
737
|
+
if (!ctx) {
|
|
738
|
+
return { hash: '' };
|
|
739
|
+
}
|
|
740
|
+
// Draw background
|
|
741
|
+
ctx.fillStyle = '#f8f8f8';
|
|
742
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
743
|
+
// Draw gradient
|
|
744
|
+
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
|
745
|
+
gradient.addColorStop(0, '#ff6b6b');
|
|
746
|
+
gradient.addColorStop(0.5, '#4ecdc4');
|
|
747
|
+
gradient.addColorStop(1, '#45b7d1');
|
|
748
|
+
ctx.fillStyle = gradient;
|
|
749
|
+
ctx.fillRect(10, 10, 100, 50);
|
|
750
|
+
// Draw text with different fonts
|
|
751
|
+
ctx.font = '18px Arial';
|
|
752
|
+
ctx.fillStyle = '#333';
|
|
753
|
+
ctx.textBaseline = 'alphabetic';
|
|
754
|
+
ctx.fillText('Seekora Fingerprint 🔐', 20, 90);
|
|
755
|
+
// Draw more text with different styling
|
|
756
|
+
ctx.font = 'bold 14px Georgia';
|
|
757
|
+
ctx.fillStyle = 'rgba(102, 204, 153, 0.7)';
|
|
758
|
+
ctx.fillText('Test String @#$%', 130, 40);
|
|
759
|
+
// Draw shapes
|
|
760
|
+
ctx.beginPath();
|
|
761
|
+
ctx.arc(200, 70, 30, 0, Math.PI * 2);
|
|
762
|
+
ctx.fillStyle = 'rgba(255, 165, 0, 0.5)';
|
|
763
|
+
ctx.fill();
|
|
764
|
+
ctx.strokeStyle = '#333';
|
|
765
|
+
ctx.lineWidth = 2;
|
|
766
|
+
ctx.stroke();
|
|
767
|
+
// Draw bezier curve
|
|
768
|
+
ctx.beginPath();
|
|
769
|
+
ctx.moveTo(10, 120);
|
|
770
|
+
ctx.bezierCurveTo(50, 80, 100, 120, 140, 100);
|
|
771
|
+
ctx.strokeStyle = '#9b59b6';
|
|
772
|
+
ctx.lineWidth = 3;
|
|
773
|
+
ctx.stroke();
|
|
774
|
+
// Draw rotated rectangle
|
|
775
|
+
ctx.save();
|
|
776
|
+
ctx.translate(180, 100);
|
|
777
|
+
ctx.rotate(0.5);
|
|
778
|
+
ctx.fillStyle = 'rgba(52, 152, 219, 0.6)';
|
|
779
|
+
ctx.fillRect(-20, -10, 40, 20);
|
|
780
|
+
ctx.restore();
|
|
781
|
+
const data = canvas.toDataURL('image/png');
|
|
782
|
+
const hash = murmurHash3Hex(data);
|
|
783
|
+
return { hash, data };
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
return { hash: '' };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* WebGL fingerprinting - extracts GPU info and renders test triangles
|
|
791
|
+
*/
|
|
792
|
+
async getWebGLFingerprint() {
|
|
793
|
+
try {
|
|
794
|
+
const canvas = document.createElement('canvas');
|
|
795
|
+
canvas.width = 256;
|
|
796
|
+
canvas.height = 256;
|
|
797
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
798
|
+
if (!gl) {
|
|
799
|
+
return { renderer: '', vendor: '', hash: '' };
|
|
800
|
+
}
|
|
801
|
+
// Get debug info extension for renderer/vendor
|
|
802
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
803
|
+
const renderer = debugInfo
|
|
804
|
+
? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || ''
|
|
805
|
+
: '';
|
|
806
|
+
const vendor = debugInfo
|
|
807
|
+
? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || ''
|
|
808
|
+
: '';
|
|
809
|
+
// Collect WebGL parameters
|
|
810
|
+
const params = [];
|
|
811
|
+
// Basic parameters
|
|
812
|
+
const paramIds = [
|
|
813
|
+
gl.ALIASED_LINE_WIDTH_RANGE,
|
|
814
|
+
gl.ALIASED_POINT_SIZE_RANGE,
|
|
815
|
+
gl.ALPHA_BITS,
|
|
816
|
+
gl.BLUE_BITS,
|
|
817
|
+
gl.DEPTH_BITS,
|
|
818
|
+
gl.GREEN_BITS,
|
|
819
|
+
gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS,
|
|
820
|
+
gl.MAX_CUBE_MAP_TEXTURE_SIZE,
|
|
821
|
+
gl.MAX_FRAGMENT_UNIFORM_VECTORS,
|
|
822
|
+
gl.MAX_RENDERBUFFER_SIZE,
|
|
823
|
+
gl.MAX_TEXTURE_IMAGE_UNITS,
|
|
824
|
+
gl.MAX_TEXTURE_SIZE,
|
|
825
|
+
gl.MAX_VARYING_VECTORS,
|
|
826
|
+
gl.MAX_VERTEX_ATTRIBS,
|
|
827
|
+
gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS,
|
|
828
|
+
gl.MAX_VERTEX_UNIFORM_VECTORS,
|
|
829
|
+
gl.MAX_VIEWPORT_DIMS,
|
|
830
|
+
gl.RED_BITS,
|
|
831
|
+
gl.RENDERER,
|
|
832
|
+
gl.SHADING_LANGUAGE_VERSION,
|
|
833
|
+
gl.STENCIL_BITS,
|
|
834
|
+
gl.VENDOR,
|
|
835
|
+
gl.VERSION,
|
|
836
|
+
];
|
|
837
|
+
for (const paramId of paramIds) {
|
|
838
|
+
try {
|
|
839
|
+
const value = gl.getParameter(paramId);
|
|
840
|
+
if (value !== null && value !== undefined) {
|
|
841
|
+
if (value instanceof Float32Array || value instanceof Int32Array) {
|
|
842
|
+
params.push(Array.from(value).join(','));
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
params.push(String(value));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// Parameter not available
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// Get supported extensions
|
|
854
|
+
const extensions = gl.getSupportedExtensions() || [];
|
|
855
|
+
params.push(extensions.sort().join(','));
|
|
856
|
+
// Render a test triangle and get pixel data
|
|
857
|
+
try {
|
|
858
|
+
const vertexShaderSource = `
|
|
859
|
+
attribute vec2 position;
|
|
860
|
+
void main() {
|
|
861
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
862
|
+
}
|
|
863
|
+
`;
|
|
864
|
+
const fragmentShaderSource = `
|
|
865
|
+
precision mediump float;
|
|
866
|
+
void main() {
|
|
867
|
+
gl_FragColor = vec4(0.812, 0.373, 0.784, 1.0);
|
|
868
|
+
}
|
|
869
|
+
`;
|
|
870
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
871
|
+
gl.shaderSource(vertexShader, vertexShaderSource);
|
|
872
|
+
gl.compileShader(vertexShader);
|
|
873
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
874
|
+
gl.shaderSource(fragmentShader, fragmentShaderSource);
|
|
875
|
+
gl.compileShader(fragmentShader);
|
|
876
|
+
const program = gl.createProgram();
|
|
877
|
+
gl.attachShader(program, vertexShader);
|
|
878
|
+
gl.attachShader(program, fragmentShader);
|
|
879
|
+
gl.linkProgram(program);
|
|
880
|
+
gl.useProgram(program);
|
|
881
|
+
const vertices = new Float32Array([
|
|
882
|
+
0.0, 0.5,
|
|
883
|
+
-0.5, -0.5,
|
|
884
|
+
0.5, -0.5,
|
|
885
|
+
]);
|
|
886
|
+
const buffer = gl.createBuffer();
|
|
887
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
888
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
889
|
+
const positionLocation = gl.getAttribLocation(program, 'position');
|
|
890
|
+
gl.enableVertexAttribArray(positionLocation);
|
|
891
|
+
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
892
|
+
gl.clearColor(0.1, 0.1, 0.1, 1.0);
|
|
893
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
894
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
895
|
+
const pixels = new Uint8Array(256 * 256 * 4);
|
|
896
|
+
gl.readPixels(0, 0, 256, 256, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
897
|
+
// Sample some pixels for the hash
|
|
898
|
+
const sampleIndices = [0, 1000, 5000, 10000, 25000, 50000, 100000, 200000];
|
|
899
|
+
for (const idx of sampleIndices) {
|
|
900
|
+
if (idx < pixels.length) {
|
|
901
|
+
params.push(String(pixels[idx]));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
// Shader compilation/rendering failed
|
|
907
|
+
}
|
|
908
|
+
const dataString = `${renderer}|${vendor}|${params.join('|')}`;
|
|
909
|
+
const hash = murmurHash3Hex(dataString);
|
|
910
|
+
return { renderer, vendor, hash };
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
return { renderer: '', vendor: '', hash: '' };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Audio fingerprinting using AudioContext oscillator
|
|
918
|
+
*/
|
|
919
|
+
async getAudioFingerprint() {
|
|
920
|
+
try {
|
|
921
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
922
|
+
if (!AudioContext) {
|
|
923
|
+
return { hash: '' };
|
|
924
|
+
}
|
|
925
|
+
const context = new AudioContext();
|
|
926
|
+
// Create oscillator
|
|
927
|
+
const oscillator = context.createOscillator();
|
|
928
|
+
oscillator.type = 'triangle';
|
|
929
|
+
oscillator.frequency.setValueAtTime(10000, context.currentTime);
|
|
930
|
+
// Create compressor
|
|
931
|
+
const compressor = context.createDynamicsCompressor();
|
|
932
|
+
compressor.threshold.setValueAtTime(-50, context.currentTime);
|
|
933
|
+
compressor.knee.setValueAtTime(40, context.currentTime);
|
|
934
|
+
compressor.ratio.setValueAtTime(12, context.currentTime);
|
|
935
|
+
compressor.attack.setValueAtTime(0, context.currentTime);
|
|
936
|
+
compressor.release.setValueAtTime(0.25, context.currentTime);
|
|
937
|
+
// Create analyser
|
|
938
|
+
const analyser = context.createAnalyser();
|
|
939
|
+
analyser.fftSize = 2048;
|
|
940
|
+
// Connect nodes
|
|
941
|
+
oscillator.connect(compressor);
|
|
942
|
+
compressor.connect(analyser);
|
|
943
|
+
analyser.connect(context.destination);
|
|
944
|
+
// Start and get data
|
|
945
|
+
oscillator.start(0);
|
|
946
|
+
return new Promise((resolve) => {
|
|
947
|
+
const timeout = setTimeout(() => {
|
|
948
|
+
try {
|
|
949
|
+
oscillator.stop();
|
|
950
|
+
context.close();
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// Ignore cleanup errors
|
|
954
|
+
}
|
|
955
|
+
resolve({ hash: '' });
|
|
956
|
+
}, this.config.timeout);
|
|
957
|
+
// Give it a moment to process
|
|
958
|
+
setTimeout(() => {
|
|
959
|
+
try {
|
|
960
|
+
const dataArray = new Float32Array(analyser.frequencyBinCount);
|
|
961
|
+
analyser.getFloatFrequencyData(dataArray);
|
|
962
|
+
// Calculate a fingerprint value from the audio data
|
|
963
|
+
let sum = 0;
|
|
964
|
+
let count = 0;
|
|
965
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
966
|
+
if (isFinite(dataArray[i]) && dataArray[i] !== 0) {
|
|
967
|
+
sum += Math.abs(dataArray[i]);
|
|
968
|
+
count++;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const value = count > 0 ? sum / count : 0;
|
|
972
|
+
const dataString = Array.from(dataArray.slice(0, 100))
|
|
973
|
+
.filter(v => isFinite(v))
|
|
974
|
+
.map(v => v.toFixed(4))
|
|
975
|
+
.join(',');
|
|
976
|
+
clearTimeout(timeout);
|
|
977
|
+
oscillator.stop();
|
|
978
|
+
context.close();
|
|
979
|
+
const hash = murmurHash3Hex(dataString + String(value));
|
|
980
|
+
resolve({ hash, value });
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
clearTimeout(timeout);
|
|
984
|
+
try {
|
|
985
|
+
oscillator.stop();
|
|
986
|
+
context.close();
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
// Ignore cleanup errors
|
|
990
|
+
}
|
|
991
|
+
resolve({ hash: '' });
|
|
992
|
+
}
|
|
993
|
+
}, 200);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
return { hash: '' };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Font detection by measuring text rendering differences
|
|
1002
|
+
*/
|
|
1003
|
+
detectFonts() {
|
|
1004
|
+
const detectedFonts = [];
|
|
1005
|
+
try {
|
|
1006
|
+
const baseFonts = ['monospace', 'sans-serif', 'serif'];
|
|
1007
|
+
const testString = 'mmmmmmmmmmlli';
|
|
1008
|
+
const testSize = '72px';
|
|
1009
|
+
const canvas = document.createElement('canvas');
|
|
1010
|
+
canvas.width = 500;
|
|
1011
|
+
canvas.height = 100;
|
|
1012
|
+
const ctx = canvas.getContext('2d');
|
|
1013
|
+
if (!ctx) {
|
|
1014
|
+
return detectedFonts;
|
|
1015
|
+
}
|
|
1016
|
+
// Get base widths
|
|
1017
|
+
const baseWidths = {};
|
|
1018
|
+
for (const baseFont of baseFonts) {
|
|
1019
|
+
ctx.font = `${testSize} ${baseFont}`;
|
|
1020
|
+
baseWidths[baseFont] = ctx.measureText(testString).width;
|
|
1021
|
+
}
|
|
1022
|
+
// Check each font
|
|
1023
|
+
for (const font of FONTS_TO_CHECK) {
|
|
1024
|
+
let detected = false;
|
|
1025
|
+
for (const baseFont of baseFonts) {
|
|
1026
|
+
ctx.font = `${testSize} "${font}", ${baseFont}`;
|
|
1027
|
+
const width = ctx.measureText(testString).width;
|
|
1028
|
+
if (width !== baseWidths[baseFont]) {
|
|
1029
|
+
detected = true;
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (detected) {
|
|
1034
|
+
detectedFonts.push(font);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
// Font detection failed
|
|
1040
|
+
}
|
|
1041
|
+
return detectedFonts;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Get hardware information
|
|
1045
|
+
*/
|
|
1046
|
+
getHardwareInfo() {
|
|
1047
|
+
const nav = navigator;
|
|
1048
|
+
return {
|
|
1049
|
+
concurrency: nav.hardwareConcurrency || 0,
|
|
1050
|
+
deviceMemory: nav.deviceMemory || 0,
|
|
1051
|
+
maxTouchPoints: nav.maxTouchPoints || 0,
|
|
1052
|
+
platform: nav.platform || '',
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get screen information
|
|
1057
|
+
*/
|
|
1058
|
+
getScreenInfo() {
|
|
1059
|
+
return {
|
|
1060
|
+
width: screen.width || 0,
|
|
1061
|
+
height: screen.height || 0,
|
|
1062
|
+
colorDepth: screen.colorDepth || 0,
|
|
1063
|
+
pixelRatio: window.devicePixelRatio || 1,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Get browser information
|
|
1068
|
+
*/
|
|
1069
|
+
getBrowserInfo() {
|
|
1070
|
+
const nav = navigator;
|
|
1071
|
+
return {
|
|
1072
|
+
language: nav.language || '',
|
|
1073
|
+
languages: Array.from(nav.languages || []),
|
|
1074
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
|
|
1075
|
+
timezoneOffset: new Date().getTimezoneOffset(),
|
|
1076
|
+
cookieEnabled: nav.cookieEnabled ?? false,
|
|
1077
|
+
doNotTrack: nav.doNotTrack === '1' || nav.doNotTrack === 'yes',
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Detect if an ad blocker is installed
|
|
1082
|
+
*/
|
|
1083
|
+
async detectAdBlocker() {
|
|
1084
|
+
try {
|
|
1085
|
+
// Create a bait element
|
|
1086
|
+
const bait = document.createElement('div');
|
|
1087
|
+
bait.className = 'adsbox ad-banner ad_banner textAd text_ad text-ad';
|
|
1088
|
+
bait.style.cssText = 'position: absolute; left: -9999px; width: 1px; height: 1px;';
|
|
1089
|
+
bait.innerHTML = ' ';
|
|
1090
|
+
document.body.appendChild(bait);
|
|
1091
|
+
// Wait a bit for ad blockers to act
|
|
1092
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1093
|
+
const isBlocked = bait.offsetParent === null ||
|
|
1094
|
+
bait.offsetHeight === 0 ||
|
|
1095
|
+
bait.offsetWidth === 0 ||
|
|
1096
|
+
getComputedStyle(bait).display === 'none' ||
|
|
1097
|
+
getComputedStyle(bait).visibility === 'hidden';
|
|
1098
|
+
document.body.removeChild(bait);
|
|
1099
|
+
// Also try to fetch a known ad-related resource
|
|
1100
|
+
try {
|
|
1101
|
+
const response = await fetch('https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js', {
|
|
1102
|
+
method: 'HEAD',
|
|
1103
|
+
mode: 'no-cors',
|
|
1104
|
+
});
|
|
1105
|
+
// If we get here without error, no ad blocker (though no-cors doesn't give us much info)
|
|
1106
|
+
return isBlocked;
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
return true; // Blocked
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
catch {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Detect if browser is in incognito/private mode
|
|
1118
|
+
*/
|
|
1119
|
+
async detectIncognito() {
|
|
1120
|
+
// Chrome/Chromium detection
|
|
1121
|
+
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
|
1122
|
+
try {
|
|
1123
|
+
const estimate = await navigator.storage.estimate();
|
|
1124
|
+
// In incognito, quota is typically limited to ~120MB
|
|
1125
|
+
if (estimate.quota && estimate.quota < 130 * 1024 * 1024) {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
// Storage API not available
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
// Firefox detection using IndexedDB
|
|
1134
|
+
try {
|
|
1135
|
+
const db = indexedDB.open('test-private-mode');
|
|
1136
|
+
return new Promise((resolve) => {
|
|
1137
|
+
db.onerror = () => resolve(true);
|
|
1138
|
+
db.onsuccess = () => {
|
|
1139
|
+
db.result.close();
|
|
1140
|
+
indexedDB.deleteDatabase('test-private-mode');
|
|
1141
|
+
resolve(false);
|
|
1142
|
+
};
|
|
1143
|
+
// Timeout fallback
|
|
1144
|
+
setTimeout(() => resolve(false), 500);
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
// IndexedDB not available - might be incognito
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Generate a hash from any data
|
|
1154
|
+
*/
|
|
1155
|
+
generateHash(data) {
|
|
1156
|
+
return murmurHash3Hex(data);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Generate the final visitor ID from all components
|
|
1160
|
+
*/
|
|
1161
|
+
generateVisitorId(components) {
|
|
1162
|
+
const data = JSON.stringify({
|
|
1163
|
+
canvas: components.canvas.hash,
|
|
1164
|
+
webgl: components.webgl.hash,
|
|
1165
|
+
audio: components.audio.hash,
|
|
1166
|
+
fonts: components.fonts.sort().join(','),
|
|
1167
|
+
hardware: components.hardware,
|
|
1168
|
+
screen: components.screen,
|
|
1169
|
+
browser: {
|
|
1170
|
+
...components.browser,
|
|
1171
|
+
languages: components.browser.languages.sort().join(','),
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
// Generate a 32-character hex string (similar to UUID format)
|
|
1175
|
+
const hash1 = murmurHash3Hex(data, 0);
|
|
1176
|
+
const hash2 = murmurHash3Hex(data, 1);
|
|
1177
|
+
const hash3 = murmurHash3Hex(data, 2);
|
|
1178
|
+
const hash4 = murmurHash3Hex(data, 3);
|
|
1179
|
+
return `${hash1}${hash2}${hash3}${hash4}`;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Calculate confidence score based on available components
|
|
1183
|
+
*/
|
|
1184
|
+
calculateConfidence(components) {
|
|
1185
|
+
let score = 0;
|
|
1186
|
+
let maxScore = 0;
|
|
1187
|
+
// Canvas (high entropy)
|
|
1188
|
+
maxScore += 0.2;
|
|
1189
|
+
if (components.canvas.hash) {
|
|
1190
|
+
score += 0.2;
|
|
1191
|
+
}
|
|
1192
|
+
// WebGL (high entropy)
|
|
1193
|
+
maxScore += 0.2;
|
|
1194
|
+
if (components.webgl.hash && components.webgl.renderer) {
|
|
1195
|
+
score += 0.2;
|
|
1196
|
+
}
|
|
1197
|
+
else if (components.webgl.hash) {
|
|
1198
|
+
score += 0.1;
|
|
1199
|
+
}
|
|
1200
|
+
// Audio (medium entropy)
|
|
1201
|
+
maxScore += 0.15;
|
|
1202
|
+
if (components.audio.hash) {
|
|
1203
|
+
score += 0.15;
|
|
1204
|
+
}
|
|
1205
|
+
// Fonts (high entropy)
|
|
1206
|
+
maxScore += 0.15;
|
|
1207
|
+
if (components.fonts.length > 10) {
|
|
1208
|
+
score += 0.15;
|
|
1209
|
+
}
|
|
1210
|
+
else if (components.fonts.length > 5) {
|
|
1211
|
+
score += 0.1;
|
|
1212
|
+
}
|
|
1213
|
+
else if (components.fonts.length > 0) {
|
|
1214
|
+
score += 0.05;
|
|
1215
|
+
}
|
|
1216
|
+
// Hardware (medium entropy)
|
|
1217
|
+
maxScore += 0.1;
|
|
1218
|
+
if (components.hardware.concurrency > 0 && components.hardware.platform) {
|
|
1219
|
+
score += 0.1;
|
|
1220
|
+
}
|
|
1221
|
+
else if (components.hardware.platform) {
|
|
1222
|
+
score += 0.05;
|
|
1223
|
+
}
|
|
1224
|
+
// Screen (low entropy but stable)
|
|
1225
|
+
maxScore += 0.1;
|
|
1226
|
+
if (components.screen.width > 0 && components.screen.height > 0) {
|
|
1227
|
+
score += 0.1;
|
|
1228
|
+
}
|
|
1229
|
+
// Browser (low entropy but stable)
|
|
1230
|
+
maxScore += 0.1;
|
|
1231
|
+
if (components.browser.timezone && components.browser.language) {
|
|
1232
|
+
score += 0.1;
|
|
1233
|
+
}
|
|
1234
|
+
else if (components.browser.language) {
|
|
1235
|
+
score += 0.05;
|
|
1236
|
+
}
|
|
1237
|
+
// Normalize and return
|
|
1238
|
+
return Math.min(1, score / maxScore);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// ============================================================================
|
|
1242
|
+
// Convenience Function
|
|
1243
|
+
// ============================================================================
|
|
1244
|
+
/**
|
|
1245
|
+
* Quick fingerprint getter - creates a Fingerprint instance and returns the result
|
|
1246
|
+
*/
|
|
1247
|
+
async function getFingerprint(config) {
|
|
1248
|
+
const fingerprint = new Fingerprint(config);
|
|
1249
|
+
return fingerprint.get();
|
|
1250
|
+
}
|
|
1251
|
+
|
|
568
1252
|
/**
|
|
569
1253
|
* Default Theme
|
|
570
1254
|
*/
|
|
@@ -1133,38 +1817,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
1133
1817
|
const state = { currentPage, itemsPerPage: stateItemsPerPage };
|
|
1134
1818
|
// Calculate absolute position (1-based) accounting for pagination
|
|
1135
1819
|
const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
hasContext: !!searchContext,
|
|
1148
|
-
});
|
|
1149
|
-
await client.trackEvent?.({
|
|
1150
|
-
event_name: 'product_click',
|
|
1151
|
-
clicked_item_id: result.id,
|
|
1152
|
-
metadata: {
|
|
1153
|
-
result: result,
|
|
1154
|
-
position: absolutePosition,
|
|
1155
|
-
},
|
|
1156
|
-
}, searchContext);
|
|
1157
|
-
console.log('🟢 SearchResults: Analytics event tracked successfully', {
|
|
1158
|
-
resultId: result.id,
|
|
1159
|
-
position: absolutePosition,
|
|
1160
|
-
});
|
|
1820
|
+
const searchContext = results?.context;
|
|
1821
|
+
if (client.trackClick) {
|
|
1822
|
+
await client.trackClick(result.id, absolutePosition, searchContext);
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
await client.trackEvent?.({
|
|
1826
|
+
event_name: 'product_click',
|
|
1827
|
+
clicked_item_id: result.id,
|
|
1828
|
+
metadata: { result, position: absolutePosition },
|
|
1829
|
+
}, searchContext);
|
|
1830
|
+
}
|
|
1161
1831
|
}
|
|
1162
1832
|
catch (err) {
|
|
1163
1833
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1164
|
-
|
|
1165
|
-
resultId: result.id,
|
|
1166
|
-
error: error.message,
|
|
1167
|
-
});
|
|
1834
|
+
log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
|
|
1168
1835
|
}
|
|
1169
1836
|
}
|
|
1170
1837
|
// Call user-provided callback
|
|
@@ -1224,38 +1891,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
1224
1891
|
const state = { currentPage, itemsPerPage: stateItemsPerPage };
|
|
1225
1892
|
// Calculate absolute position (1-based) accounting for pagination
|
|
1226
1893
|
const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
hasContext: !!searchContext,
|
|
1239
|
-
});
|
|
1240
|
-
await client.trackEvent?.({
|
|
1241
|
-
event_name: 'product_click',
|
|
1242
|
-
clicked_item_id: result.id,
|
|
1243
|
-
metadata: {
|
|
1244
|
-
result: result,
|
|
1245
|
-
position: absolutePosition,
|
|
1246
|
-
},
|
|
1247
|
-
}, searchContext);
|
|
1248
|
-
console.log('🟢 SearchResults: Analytics event tracked successfully', {
|
|
1249
|
-
resultId: result.id,
|
|
1250
|
-
position: absolutePosition,
|
|
1251
|
-
});
|
|
1894
|
+
const searchContext = results?.context;
|
|
1895
|
+
if (client.trackClick) {
|
|
1896
|
+
await client.trackClick(result.id, absolutePosition, searchContext);
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
await client.trackEvent?.({
|
|
1900
|
+
event_name: 'product_click',
|
|
1901
|
+
clicked_item_id: result.id,
|
|
1902
|
+
metadata: { result, position: absolutePosition },
|
|
1903
|
+
}, searchContext);
|
|
1904
|
+
}
|
|
1252
1905
|
}
|
|
1253
1906
|
catch (err) {
|
|
1254
1907
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1255
|
-
|
|
1256
|
-
resultId: result.id,
|
|
1257
|
-
error: error.message,
|
|
1258
|
-
});
|
|
1908
|
+
log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
|
|
1259
1909
|
}
|
|
1260
1910
|
}
|
|
1261
1911
|
// Call user-provided callback
|
|
@@ -2613,7 +3263,7 @@ const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, s
|
|
|
2613
3263
|
*
|
|
2614
3264
|
* Renders highlighted search result text with matching terms emphasized
|
|
2615
3265
|
*/
|
|
2616
|
-
const Highlight = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
|
|
3266
|
+
const Highlight$1 = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
|
|
2617
3267
|
const { theme } = useSearchContext();
|
|
2618
3268
|
const highlightTheme = customTheme || {};
|
|
2619
3269
|
// Get highlighted value from hit
|
|
@@ -3914,7 +4564,7 @@ function transformFilteredTab(raw) {
|
|
|
3914
4564
|
// Main Hook
|
|
3915
4565
|
// ============================================================================
|
|
3916
4566
|
function useQuerySuggestionsEnhanced(options) {
|
|
3917
|
-
const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeCategories =
|
|
4567
|
+
const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeCategories = true, includeFacets = false, maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
|
|
3918
4568
|
// State
|
|
3919
4569
|
const [suggestions, setSuggestions] = useState([]);
|
|
3920
4570
|
const [loading, setLoading] = useState(false);
|
|
@@ -5316,7 +5966,7 @@ const ImagePlaceholder = () => (React.createElement("svg", { viewBox: "0 0 24 24
|
|
|
5316
5966
|
// Component
|
|
5317
5967
|
// ============================================================================
|
|
5318
5968
|
const FederatedDropdown = forwardRef(function FederatedDropdown(props, ref) {
|
|
5319
|
-
const { query, isOpen = true, maxSuggestions = 8, maxProducts = 8, maxBrands = 8, minQueryLength = 0, debounceMs = 200, filteredTabs: tabsConfig, showProducts = true, showBrands = true, showFilteredTabs = true, showRecentSearches = true, layout = 'side-by-side', productsColumnWidth = '60%', suggestionsColumnWidth = '40%', showPrices = true, currencySymbol = '$', classNames = {}, style, renderProduct, renderSuggestion, renderBrand, renderTab, header, footer, viewAllProductsLink, width = '800px', maxHeight = '600px', zIndex = 1000, analyticsTags, onSuggestionSelect, onProductClick, onBrandClick, onTabSelect, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, } = props;
|
|
5969
|
+
const { query, isOpen = true, maxSuggestions = 8, maxProducts = 8, maxBrands = 8, minQueryLength = 0, debounceMs = 200, filteredTabs: tabsConfig, showProducts = true, showBrands = true, showFilteredTabs = true, showRecentSearches = true, layout = 'side-by-side', productsColumnWidth = '60%', suggestionsColumnWidth = '40%', showPrices = true, currencySymbol = '$', classNames = {}, style, renderProduct, renderSuggestion, renderBrand, renderTab, header, footer, viewAllProductsLink, width = '800px', maxHeight = '600px', zIndex = 1000, analyticsTags, includeFacets: includeFacetsProp = true, includeCategories: includeCategoriesProp = true, includeDropdownRecommendations: includeDropdownRecommendationsProp = true, onSuggestionSelect, onProductClick, onBrandClick, onTabSelect, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, } = props;
|
|
5320
5970
|
const { client } = useSearchContext();
|
|
5321
5971
|
const containerRef = useRef(null);
|
|
5322
5972
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
@@ -5331,7 +5981,9 @@ const FederatedDropdown = forwardRef(function FederatedDropdown(props, ref) {
|
|
|
5331
5981
|
debounceMs,
|
|
5332
5982
|
maxSuggestions,
|
|
5333
5983
|
minQueryLength,
|
|
5334
|
-
includeDropdownRecommendations:
|
|
5984
|
+
includeDropdownRecommendations: includeDropdownRecommendationsProp,
|
|
5985
|
+
includeCategories: includeCategoriesProp,
|
|
5986
|
+
includeFacets: includeFacetsProp,
|
|
5335
5987
|
filteredTabs: tabsConfig,
|
|
5336
5988
|
analyticsTags,
|
|
5337
5989
|
enableRecentSearches: showRecentSearches,
|
|
@@ -5587,7 +6239,7 @@ const EVENTS = {
|
|
|
5587
6239
|
// Hook Implementation
|
|
5588
6240
|
// ============================================================================
|
|
5589
6241
|
function useSuggestionsAnalytics(options) {
|
|
5590
|
-
const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, } = options;
|
|
6242
|
+
const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, context: contextOption, } = options;
|
|
5591
6243
|
// Refs for debouncing and tracking
|
|
5592
6244
|
const impressionTimerRef = useRef(null);
|
|
5593
6245
|
const lastImpressionRef = useRef(null);
|
|
@@ -5600,10 +6252,11 @@ function useSuggestionsAnalytics(options) {
|
|
|
5600
6252
|
}
|
|
5601
6253
|
};
|
|
5602
6254
|
}, []);
|
|
5603
|
-
// Helper to send event
|
|
5604
|
-
const sendEvent = useCallback(async (eventName, metadata) => {
|
|
6255
|
+
// Helper to send event (optional context links event to search for v3 analytics)
|
|
6256
|
+
const sendEvent = useCallback(async (eventName, metadata, context) => {
|
|
5605
6257
|
if (!enabled || !client)
|
|
5606
6258
|
return;
|
|
6259
|
+
const searchContext = context ?? contextOption;
|
|
5607
6260
|
try {
|
|
5608
6261
|
await client.trackEvent?.({
|
|
5609
6262
|
event_name: eventName,
|
|
@@ -5613,13 +6266,13 @@ function useSuggestionsAnalytics(options) {
|
|
|
5613
6266
|
timestamp: Date.now(),
|
|
5614
6267
|
source: 'suggestions_dropdown',
|
|
5615
6268
|
},
|
|
5616
|
-
});
|
|
6269
|
+
}, searchContext);
|
|
5617
6270
|
log.verbose(`Analytics: ${eventName}`, metadata);
|
|
5618
6271
|
}
|
|
5619
6272
|
catch (error) {
|
|
5620
6273
|
log.warn(`Failed to track ${eventName}`, { error });
|
|
5621
6274
|
}
|
|
5622
|
-
}, [client, enabled, analyticsTags]);
|
|
6275
|
+
}, [client, enabled, analyticsTags, contextOption]);
|
|
5623
6276
|
// Track suggestion click
|
|
5624
6277
|
const trackSuggestionClick = useCallback((data) => {
|
|
5625
6278
|
if (!trackClicks)
|
|
@@ -5647,11 +6300,12 @@ function useSuggestionsAnalytics(options) {
|
|
|
5647
6300
|
tab_id: data.tabId,
|
|
5648
6301
|
original_query: data.query,
|
|
5649
6302
|
});
|
|
5650
|
-
// Also track as a general product click for analytics
|
|
5651
|
-
|
|
5652
|
-
|
|
6303
|
+
// Also track as a general product click for analytics (with context when available)
|
|
6304
|
+
const searchContext = contextOption;
|
|
6305
|
+
if (client?.trackClick) {
|
|
6306
|
+
Promise.resolve(client.trackClick(data.product.id || data.product.objectID || '', data.position + 1, searchContext)).catch(() => { });
|
|
5653
6307
|
}
|
|
5654
|
-
}, [client, sendEvent, trackClicks]);
|
|
6308
|
+
}, [client, contextOption, sendEvent, trackClicks]);
|
|
5655
6309
|
// Track category click
|
|
5656
6310
|
const trackCategoryClick = useCallback((category, query) => {
|
|
5657
6311
|
if (!trackClicks)
|
|
@@ -5868,7 +6522,7 @@ const ClearIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill
|
|
|
5868
6522
|
// Component
|
|
5869
6523
|
// ============================================================================
|
|
5870
6524
|
const SearchBarWithSuggestions = forwardRef(function SearchBarWithSuggestions(props, ref) {
|
|
5871
|
-
const { variant = 'classic', placeholder = 'Search...', initialQuery = '', value, onQueryChange, onSearch, onSuggestionSelect, onProductClick, showSearchButton = false, searchButtonText = 'Search', showClearButton = true, autoFocus = false, minQueryLength = 1, maxSuggestions = 8, debounceMs = 200, showRecentSearches = true, showTrendingOnEmpty = true, includeDropdownRecommendations = false, filteredTabs, enableAnalytics = true, analyticsTags, dropdownWidth, dropdownMaxHeight, classNames = {}, style, inputStyle, ariaLabel = 'Search', } = props;
|
|
6525
|
+
const { variant = 'classic', placeholder = 'Search...', initialQuery = '', value, onQueryChange, onSearch, onSuggestionSelect, onProductClick, showSearchButton = false, searchButtonText = 'Search', showClearButton = true, autoFocus = false, minQueryLength = 1, maxSuggestions = 8, debounceMs = 200, showRecentSearches = true, showTrendingOnEmpty = true, includeDropdownRecommendations = false, filteredTabs, enableAnalytics = true, analyticsTags, includeFacets, includeCategories, dropdownWidth, dropdownMaxHeight, classNames = {}, style, inputStyle, ariaLabel = 'Search', } = props;
|
|
5872
6526
|
const { client } = useSearchContext();
|
|
5873
6527
|
const inputRef = useRef(null);
|
|
5874
6528
|
const dropdownRef = useRef(null);
|
|
@@ -6027,7 +6681,7 @@ const SearchBarWithSuggestions = forwardRef(function SearchBarWithSuggestions(pr
|
|
|
6027
6681
|
case 'rich':
|
|
6028
6682
|
return (React.createElement(RichQuerySuggestions, { ref: dropdownRef, ...commonProps, includeDropdownRecommendations: includeDropdownRecommendations, includeCategories: true, width: dropdownWidth || '100%', maxHeight: dropdownMaxHeight || '480px' }));
|
|
6029
6683
|
case 'federated':
|
|
6030
|
-
return (React.createElement(FederatedDropdown, { ref: dropdownRef, ...commonProps, filteredTabs: filteredTabs, showProducts: true, showBrands: true, showFilteredTabs: !!filteredTabs, onProductClick: handleProductClick, width: dropdownWidth || '800px', maxHeight: dropdownMaxHeight || '600px' }));
|
|
6684
|
+
return (React.createElement(FederatedDropdown, { ref: dropdownRef, ...commonProps, filteredTabs: filteredTabs, showProducts: true, showBrands: true, showFilteredTabs: !!filteredTabs, onProductClick: handleProductClick, width: dropdownWidth || '800px', maxHeight: dropdownMaxHeight || '600px', includeFacets: includeFacets, includeCategories: includeCategories, includeDropdownRecommendations: includeDropdownRecommendations }));
|
|
6031
6685
|
case 'compact':
|
|
6032
6686
|
return (React.createElement(QuerySuggestionsDropdown, { ref: dropdownRef, ...commonProps, maxSuggestions: 5, showCounts: false, width: dropdownWidth || '100%' }));
|
|
6033
6687
|
case 'classic':
|
|
@@ -10561,6 +11215,968 @@ const SuggestionDropdownVariants = {
|
|
|
10561
11215
|
minimal: MinimalDropdown,
|
|
10562
11216
|
};
|
|
10563
11217
|
|
|
11218
|
+
function Modal({ isOpen, onClose, children }) {
|
|
11219
|
+
const overlayRef = useRef(null);
|
|
11220
|
+
const containerRef = useRef(null);
|
|
11221
|
+
useEffect(() => {
|
|
11222
|
+
const handleClickOutside = (event) => {
|
|
11223
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
11224
|
+
onClose();
|
|
11225
|
+
}
|
|
11226
|
+
};
|
|
11227
|
+
if (isOpen)
|
|
11228
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
11229
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
11230
|
+
}, [isOpen, onClose]);
|
|
11231
|
+
useEffect(() => {
|
|
11232
|
+
if (isOpen) {
|
|
11233
|
+
const originalOverflow = document.body.style.overflow;
|
|
11234
|
+
document.body.style.overflow = 'hidden';
|
|
11235
|
+
return () => { document.body.style.overflow = originalOverflow; };
|
|
11236
|
+
}
|
|
11237
|
+
}, [isOpen]);
|
|
11238
|
+
useEffect(() => {
|
|
11239
|
+
if (!isOpen || !containerRef.current)
|
|
11240
|
+
return;
|
|
11241
|
+
const container = containerRef.current;
|
|
11242
|
+
const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
11243
|
+
const firstElement = focusableElements[0];
|
|
11244
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
11245
|
+
const handleTabKey = (e) => {
|
|
11246
|
+
if (e.key !== 'Tab')
|
|
11247
|
+
return;
|
|
11248
|
+
if (e.shiftKey) {
|
|
11249
|
+
if (document.activeElement === firstElement) {
|
|
11250
|
+
e.preventDefault();
|
|
11251
|
+
lastElement?.focus();
|
|
11252
|
+
}
|
|
11253
|
+
}
|
|
11254
|
+
else {
|
|
11255
|
+
if (document.activeElement === lastElement) {
|
|
11256
|
+
e.preventDefault();
|
|
11257
|
+
firstElement?.focus();
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
};
|
|
11261
|
+
container.addEventListener('keydown', handleTabKey);
|
|
11262
|
+
return () => container.removeEventListener('keydown', handleTabKey);
|
|
11263
|
+
}, [isOpen]);
|
|
11264
|
+
if (!isOpen || typeof document === 'undefined')
|
|
11265
|
+
return null;
|
|
11266
|
+
const modalContent = (React.createElement("div", { ref: overlayRef, className: "seekora-docsearch-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Search documentation" },
|
|
11267
|
+
React.createElement("div", { ref: containerRef, className: "seekora-docsearch-container" }, children)));
|
|
11268
|
+
return createPortal(modalContent, document.body);
|
|
11269
|
+
}
|
|
11270
|
+
|
|
11271
|
+
function SearchBox({ value, onChange, onKeyDown, placeholder = 'Search documentation...', isLoading = false, onClear, }) {
|
|
11272
|
+
const inputRef = useRef(null);
|
|
11273
|
+
useEffect(() => { if (inputRef.current)
|
|
11274
|
+
inputRef.current.focus(); }, []);
|
|
11275
|
+
const handleChange = (event) => onChange(event.target.value);
|
|
11276
|
+
const handleClear = () => { onChange(''); onClear?.(); inputRef.current?.focus(); };
|
|
11277
|
+
return (React.createElement("div", { className: "seekora-docsearch-searchbox" },
|
|
11278
|
+
React.createElement("label", { className: "seekora-docsearch-searchbox-icon", htmlFor: "seekora-docsearch-input" }, isLoading ? (React.createElement("span", { className: "seekora-docsearch-spinner", "aria-hidden": "true" },
|
|
11279
|
+
React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20" },
|
|
11280
|
+
React.createElement("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "40", strokeDashoffset: "10" },
|
|
11281
|
+
React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 10 10", to: "360 10 10", dur: "0.8s", repeatCount: "indefinite" }))))) : (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
|
|
11282
|
+
React.createElement("path", { d: "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z", fill: "currentColor" })))),
|
|
11283
|
+
React.createElement("input", { ref: inputRef, id: "seekora-docsearch-input", className: "seekora-docsearch-input", type: "text", value: value, onChange: handleChange, onKeyDown: onKeyDown, placeholder: placeholder, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, "aria-autocomplete": "list", "aria-controls": "seekora-docsearch-results" }),
|
|
11284
|
+
value && (React.createElement("button", { type: "button", className: "seekora-docsearch-clear", onClick: handleClear, "aria-label": "Clear search" },
|
|
11285
|
+
React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
|
|
11286
|
+
React.createElement("path", { d: "M4.28 3.22a.75.75 0 00-1.06 1.06L6.94 8l-3.72 3.72a.75.75 0 101.06 1.06L8 9.06l3.72 3.72a.75.75 0 101.06-1.06L9.06 8l3.72-3.72a.75.75 0 00-1.06-1.06L8 6.94 4.28 3.22z", fill: "currentColor" }))))));
|
|
11287
|
+
}
|
|
11288
|
+
|
|
11289
|
+
function sanitizeHtml(html) {
|
|
11290
|
+
const escaped = html
|
|
11291
|
+
.replace(/&/g, '&')
|
|
11292
|
+
.replace(/</g, '<')
|
|
11293
|
+
.replace(/>/g, '>')
|
|
11294
|
+
.replace(/"/g, '"')
|
|
11295
|
+
.replace(/'/g, ''');
|
|
11296
|
+
return escaped
|
|
11297
|
+
.replace(/<mark>/g, '<mark>')
|
|
11298
|
+
.replace(/<\/mark>/g, '</mark>')
|
|
11299
|
+
.replace(/<ais-highlight>/g, '<mark>')
|
|
11300
|
+
.replace(/<\/ais-highlight>/g, '</mark>')
|
|
11301
|
+
.replace(/<em>/g, '<mark>')
|
|
11302
|
+
.replace(/<\/em>/g, '</mark>');
|
|
11303
|
+
}
|
|
11304
|
+
function Highlight({ value, highlightedValue }) {
|
|
11305
|
+
if (!highlightedValue)
|
|
11306
|
+
return React.createElement("span", null, value);
|
|
11307
|
+
return (React.createElement("span", { className: "seekora-docsearch-highlight", dangerouslySetInnerHTML: { __html: sanitizeHtml(highlightedValue) } }));
|
|
11308
|
+
}
|
|
11309
|
+
function truncateAroundMatch(content, maxLength = 150) {
|
|
11310
|
+
const markIndex = content.indexOf('<mark>');
|
|
11311
|
+
if (markIndex === -1 || content.length <= maxLength) {
|
|
11312
|
+
if (content.length <= maxLength)
|
|
11313
|
+
return content;
|
|
11314
|
+
return content.slice(0, maxLength) + '...';
|
|
11315
|
+
}
|
|
11316
|
+
const halfLength = Math.floor(maxLength / 2);
|
|
11317
|
+
let start = Math.max(0, markIndex - halfLength);
|
|
11318
|
+
let end = Math.min(content.length, markIndex + halfLength);
|
|
11319
|
+
if (start > 0) {
|
|
11320
|
+
const spaceIndex = content.indexOf(' ', start);
|
|
11321
|
+
if (spaceIndex !== -1 && spaceIndex < markIndex)
|
|
11322
|
+
start = spaceIndex + 1;
|
|
11323
|
+
}
|
|
11324
|
+
if (end < content.length) {
|
|
11325
|
+
const spaceIndex = content.lastIndexOf(' ', end);
|
|
11326
|
+
if (spaceIndex !== -1 && spaceIndex > markIndex)
|
|
11327
|
+
end = spaceIndex;
|
|
11328
|
+
}
|
|
11329
|
+
let result = content.slice(start, end);
|
|
11330
|
+
if (start > 0)
|
|
11331
|
+
result = '...' + result;
|
|
11332
|
+
if (end < content.length)
|
|
11333
|
+
result = result + '...';
|
|
11334
|
+
return result;
|
|
11335
|
+
}
|
|
11336
|
+
|
|
11337
|
+
function Hit({ hit, isSelected, onClick, onMouseEnter, openInNewTab, isChild, isLastChild, hierarchyType }) {
|
|
11338
|
+
const isFullHit = 'objectID' in hit;
|
|
11339
|
+
const suggestion = hit;
|
|
11340
|
+
const hitType = hierarchyType || suggestion.type;
|
|
11341
|
+
const breadcrumb = suggestion.parentTitle
|
|
11342
|
+
? `${suggestion.category || ''} › ${suggestion.parentTitle}`.replace(/^› /, '')
|
|
11343
|
+
: suggestion.category || '';
|
|
11344
|
+
const title = getTitleForType(hit, hitType);
|
|
11345
|
+
let highlightedTitle = title;
|
|
11346
|
+
let highlightedContent = hit.content || suggestion.description || '';
|
|
11347
|
+
if (isFullHit) {
|
|
11348
|
+
const fullHit = hit;
|
|
11349
|
+
if (fullHit._highlightResult) {
|
|
11350
|
+
highlightedTitle = fullHit._highlightResult.title?.value || title;
|
|
11351
|
+
highlightedContent = fullHit._highlightResult.content?.value || hit.content || '';
|
|
11352
|
+
}
|
|
11353
|
+
}
|
|
11354
|
+
else {
|
|
11355
|
+
if (suggestion.highlight) {
|
|
11356
|
+
highlightedTitle = suggestion.highlight.title || title;
|
|
11357
|
+
highlightedContent = suggestion.highlight.content || hit.content || suggestion.description || '';
|
|
11358
|
+
}
|
|
11359
|
+
}
|
|
11360
|
+
const displayContent = highlightedContent ? truncateAroundMatch(highlightedContent, 120) : '';
|
|
11361
|
+
const url = hit.url || suggestion.route || '#';
|
|
11362
|
+
const classNames = ['seekora-docsearch-hit'];
|
|
11363
|
+
if (isSelected)
|
|
11364
|
+
classNames.push('seekora-docsearch-hit--selected');
|
|
11365
|
+
if (isChild)
|
|
11366
|
+
classNames.push('seekora-docsearch-hit--child');
|
|
11367
|
+
if (isLastChild)
|
|
11368
|
+
classNames.push('seekora-docsearch-hit--last-child');
|
|
11369
|
+
return (React.createElement("a", { href: url, className: classNames.join(' '), onClick: (e) => { e.preventDefault(); onClick(); }, onMouseEnter: onMouseEnter, role: "option", "aria-selected": isSelected, target: openInNewTab ? '_blank' : undefined, rel: openInNewTab ? 'noopener noreferrer' : undefined },
|
|
11370
|
+
isChild && (React.createElement("div", { className: "seekora-docsearch-hit-tree" },
|
|
11371
|
+
React.createElement(TreeConnector, { isLast: isLastChild }))),
|
|
11372
|
+
React.createElement("div", { className: "seekora-docsearch-hit-icon" },
|
|
11373
|
+
React.createElement(HitIcon, { type: getHitTypeFromLevel(hitType) })),
|
|
11374
|
+
React.createElement("div", { className: "seekora-docsearch-hit-content" },
|
|
11375
|
+
!isChild && breadcrumb && React.createElement("span", { className: "seekora-docsearch-hit-breadcrumb" }, breadcrumb),
|
|
11376
|
+
React.createElement("span", { className: "seekora-docsearch-hit-title" },
|
|
11377
|
+
React.createElement(Highlight, { value: title, highlightedValue: highlightedTitle })),
|
|
11378
|
+
displayContent && (React.createElement("span", { className: "seekora-docsearch-hit-description" },
|
|
11379
|
+
React.createElement(Highlight, { value: hit.content || '', highlightedValue: displayContent })))),
|
|
11380
|
+
React.createElement("div", { className: "seekora-docsearch-hit-action" }, openInNewTab ? (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" },
|
|
11381
|
+
React.createElement("path", { d: "M6 3H3v10h10v-3M9 3h4v4M14 2L7 9", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))) : (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" },
|
|
11382
|
+
React.createElement("path", { d: "M6.75 3.25L11.5 8L6.75 12.75", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))))));
|
|
11383
|
+
}
|
|
11384
|
+
function getTitleForType(hit, type) {
|
|
11385
|
+
const hierarchy = hit.hierarchy || {};
|
|
11386
|
+
if (!type)
|
|
11387
|
+
return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
|
|
11388
|
+
const match = type.match(/^lvl(\d+)$/);
|
|
11389
|
+
if (match) {
|
|
11390
|
+
const level = parseInt(match[1], 10);
|
|
11391
|
+
const levelKey = `lvl${level}`;
|
|
11392
|
+
const levelTitle = hierarchy[levelKey];
|
|
11393
|
+
if (levelTitle)
|
|
11394
|
+
return levelTitle;
|
|
11395
|
+
}
|
|
11396
|
+
return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
|
|
11397
|
+
}
|
|
11398
|
+
function getHitTypeFromLevel(type) {
|
|
11399
|
+
if (!type)
|
|
11400
|
+
return 'page';
|
|
11401
|
+
const match = type.match(/^lvl(\d+)$/);
|
|
11402
|
+
if (match) {
|
|
11403
|
+
const level = parseInt(match[1], 10);
|
|
11404
|
+
if (level === 1)
|
|
11405
|
+
return 'page';
|
|
11406
|
+
if (level <= 3)
|
|
11407
|
+
return 'section';
|
|
11408
|
+
return 'content';
|
|
11409
|
+
}
|
|
11410
|
+
return 'page';
|
|
11411
|
+
}
|
|
11412
|
+
function HitIcon({ type }) {
|
|
11413
|
+
switch (type) {
|
|
11414
|
+
case 'page':
|
|
11415
|
+
return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
|
|
11416
|
+
React.createElement("path", { d: "M4.5 3.5h11a1 1 0 011 1v11a1 1 0 01-1 1h-11a1 1 0 01-1-1v-11a1 1 0 011-1z", stroke: "currentColor", strokeWidth: "1.5", fill: "none" }),
|
|
11417
|
+
React.createElement("path", { d: "M7 7h6M7 10h6M7 13h4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
|
|
11418
|
+
case 'section':
|
|
11419
|
+
return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
|
|
11420
|
+
React.createElement("path", { d: "M4 5.5h12M4 10h12M4 14.5h8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
|
|
11421
|
+
case 'content':
|
|
11422
|
+
return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
|
|
11423
|
+
React.createElement("path", { d: "M4 6h12M4 10h8M4 14h10", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
|
|
11424
|
+
}
|
|
11425
|
+
}
|
|
11426
|
+
function TreeConnector({ isLast }) {
|
|
11427
|
+
return (React.createElement("svg", { width: "16", height: "20", viewBox: "0 0 16 20", fill: "none", "aria-hidden": "true", className: "seekora-docsearch-hit-tree-icon" }, isLast ? (React.createElement("path", { d: "M8 0V10H14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })) : (React.createElement(React.Fragment, null,
|
|
11428
|
+
React.createElement("path", { d: "M8 0V20", stroke: "currentColor", strokeWidth: "1.5" }),
|
|
11429
|
+
React.createElement("path", { d: "M8 10H14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })))));
|
|
11430
|
+
}
|
|
11431
|
+
|
|
11432
|
+
function getTypeLevel(type) {
|
|
11433
|
+
if (!type)
|
|
11434
|
+
return 1;
|
|
11435
|
+
const match = type.match(/^lvl(\d+)$/);
|
|
11436
|
+
return match ? parseInt(match[1], 10) : 1;
|
|
11437
|
+
}
|
|
11438
|
+
function isChildType(type) {
|
|
11439
|
+
return getTypeLevel(type) >= 2;
|
|
11440
|
+
}
|
|
11441
|
+
/** Build a stable grouping key from all hierarchy levels (lvl0 through lvl5). */
|
|
11442
|
+
function getHierarchyKey(hit) {
|
|
11443
|
+
const h = hit.hierarchy ?? {};
|
|
11444
|
+
const parts = [
|
|
11445
|
+
h.lvl0 ?? '',
|
|
11446
|
+
h.lvl1 ?? '',
|
|
11447
|
+
h.lvl2 ?? '',
|
|
11448
|
+
h.lvl3 ?? '',
|
|
11449
|
+
h.lvl4 ?? '',
|
|
11450
|
+
h.lvl5 ?? '',
|
|
11451
|
+
];
|
|
11452
|
+
return parts.join('\0');
|
|
11453
|
+
}
|
|
11454
|
+
/** Build a display breadcrumb from hierarchy, skipping consecutive duplicates (e.g. lvl0 === lvl1). */
|
|
11455
|
+
function getHierarchyBreadcrumb(hit) {
|
|
11456
|
+
const h = hit.hierarchy ?? {};
|
|
11457
|
+
const parts = [h.lvl0, h.lvl1, h.lvl2, h.lvl3, h.lvl4, h.lvl5].filter((v) => typeof v === 'string' && v.length > 0);
|
|
11458
|
+
const deduped = [];
|
|
11459
|
+
for (const p of parts) {
|
|
11460
|
+
if (deduped[deduped.length - 1] !== p)
|
|
11461
|
+
deduped.push(p);
|
|
11462
|
+
}
|
|
11463
|
+
return deduped.join(' › ');
|
|
11464
|
+
}
|
|
11465
|
+
function groupHitsByHierarchy(hits) {
|
|
11466
|
+
const hitToIndex = new Map();
|
|
11467
|
+
hits.forEach((h, i) => hitToIndex.set(h, i));
|
|
11468
|
+
const groups = new Map();
|
|
11469
|
+
for (const hit of hits) {
|
|
11470
|
+
const key = getHierarchyKey(hit);
|
|
11471
|
+
if (!groups.has(key))
|
|
11472
|
+
groups.set(key, []);
|
|
11473
|
+
groups.get(key).push(hit);
|
|
11474
|
+
}
|
|
11475
|
+
const entries = Array.from(groups.entries());
|
|
11476
|
+
entries.sort(([, aHits], [, bHits]) => {
|
|
11477
|
+
const aMin = Math.min(...aHits.map(h => hitToIndex.get(h) ?? 0));
|
|
11478
|
+
const bMin = Math.min(...bHits.map(h => hitToIndex.get(h) ?? 0));
|
|
11479
|
+
return aMin - bMin;
|
|
11480
|
+
});
|
|
11481
|
+
const result = [];
|
|
11482
|
+
for (const [, groupHits] of entries) {
|
|
11483
|
+
const sortedHits = [...groupHits].sort((a, b) => {
|
|
11484
|
+
const aType = a.type;
|
|
11485
|
+
const bType = b.type;
|
|
11486
|
+
return getTypeLevel(aType) - getTypeLevel(bType);
|
|
11487
|
+
});
|
|
11488
|
+
const markedHits = sortedHits.map((hit, index) => {
|
|
11489
|
+
const suggestion = hit;
|
|
11490
|
+
const isChild = isChildType(suggestion.type);
|
|
11491
|
+
const nextHit = sortedHits[index + 1];
|
|
11492
|
+
const isLastChild = isChild && (!nextHit || !isChildType(nextHit.type));
|
|
11493
|
+
return { ...hit, isChild, isLastChild };
|
|
11494
|
+
});
|
|
11495
|
+
const category = getHierarchyBreadcrumb(groupHits[0]);
|
|
11496
|
+
result.push({ category: category || null, hits: markedHits });
|
|
11497
|
+
}
|
|
11498
|
+
return result;
|
|
11499
|
+
}
|
|
11500
|
+
function getGlobalIndexMulti(groups, groupIndex, hitIndex) {
|
|
11501
|
+
let index = 0;
|
|
11502
|
+
for (let i = 0; i < groupIndex; i++)
|
|
11503
|
+
index += groups[i].hits.length;
|
|
11504
|
+
return index + hitIndex;
|
|
11505
|
+
}
|
|
11506
|
+
function getHitKey(hit, index) {
|
|
11507
|
+
if ('objectID' in hit)
|
|
11508
|
+
return hit.objectID;
|
|
11509
|
+
return `suggestion-${hit.url}-${index}`;
|
|
11510
|
+
}
|
|
11511
|
+
function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations = {}, sources: _sources = [], }) {
|
|
11512
|
+
const listRef = useRef(null);
|
|
11513
|
+
useEffect(() => {
|
|
11514
|
+
if (!listRef.current || hits.length === 0)
|
|
11515
|
+
return;
|
|
11516
|
+
// Only scroll when selection changed via keyboard (avoids scroll-to-selected on hover)
|
|
11517
|
+
if (scrollSelectionIntoViewRef && !scrollSelectionIntoViewRef.current)
|
|
11518
|
+
return;
|
|
11519
|
+
// listRef's direct children are groups (li.seekora-docsearch-results-group), not hits.
|
|
11520
|
+
// Find the actual hit element at flat selectedIndex.
|
|
11521
|
+
const groupEls = listRef.current.querySelectorAll('.seekora-docsearch-results-group');
|
|
11522
|
+
let idx = 0;
|
|
11523
|
+
for (const groupEl of groupEls) {
|
|
11524
|
+
const itemList = groupEl.querySelector('.seekora-docsearch-results-group-items');
|
|
11525
|
+
if (!itemList)
|
|
11526
|
+
continue;
|
|
11527
|
+
const items = itemList.children;
|
|
11528
|
+
for (let i = 0; i < items.length; i++) {
|
|
11529
|
+
if (idx === selectedIndex) {
|
|
11530
|
+
items[i].scrollIntoView({ block: 'nearest' });
|
|
11531
|
+
if (scrollSelectionIntoViewRef)
|
|
11532
|
+
scrollSelectionIntoViewRef.current = false;
|
|
11533
|
+
return;
|
|
11534
|
+
}
|
|
11535
|
+
idx++;
|
|
11536
|
+
}
|
|
11537
|
+
}
|
|
11538
|
+
if (scrollSelectionIntoViewRef)
|
|
11539
|
+
scrollSelectionIntoViewRef.current = false;
|
|
11540
|
+
}, [selectedIndex, hits.length, scrollSelectionIntoViewRef]);
|
|
11541
|
+
if (!query) {
|
|
11542
|
+
return (React.createElement("div", { className: "seekora-docsearch-empty" },
|
|
11543
|
+
React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
|
|
11544
|
+
}
|
|
11545
|
+
if (isLoading && hits.length === 0) {
|
|
11546
|
+
return (React.createElement("div", { className: "seekora-docsearch-loading" },
|
|
11547
|
+
React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
|
|
11548
|
+
React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
11549
|
+
React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "50", strokeDashoffset: "15" },
|
|
11550
|
+
React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
|
|
11551
|
+
React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
|
|
11552
|
+
}
|
|
11553
|
+
if (error) {
|
|
11554
|
+
return (React.createElement("div", { className: "seekora-docsearch-error" },
|
|
11555
|
+
React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
|
|
11556
|
+
}
|
|
11557
|
+
if (hits.length === 0 && query) {
|
|
11558
|
+
return (React.createElement("div", { className: "seekora-docsearch-no-results" },
|
|
11559
|
+
React.createElement("p", { className: "seekora-docsearch-no-results-text" }, translations.noResultsText || `No results found for "${query}"`)));
|
|
11560
|
+
}
|
|
11561
|
+
const displayGroups = groupedHits && groupedHits.length > 0
|
|
11562
|
+
? groupedHits.map(g => ({ category: g.source.name, sourceId: g.source.id, openInNewTab: g.source.openInNewTab, hits: g.items }))
|
|
11563
|
+
: groupHitsByHierarchy(hits).map(g => ({ category: g.category, sourceId: 'default', openInNewTab: false, hits: g.hits }));
|
|
11564
|
+
return (React.createElement("div", { className: "seekora-docsearch-results" },
|
|
11565
|
+
React.createElement("ul", { ref: listRef, id: "seekora-docsearch-results", className: "seekora-docsearch-results-list", role: "listbox" }, displayGroups.map((group, groupIndex) => (React.createElement("li", { key: group.sourceId + '-' + groupIndex, className: "seekora-docsearch-results-group" },
|
|
11566
|
+
group.category && React.createElement("div", { className: "seekora-docsearch-results-group-header" }, group.category),
|
|
11567
|
+
React.createElement("ul", { className: "seekora-docsearch-results-group-items" }, group.hits.map((hit, hitIndex) => {
|
|
11568
|
+
const globalIndex = getGlobalIndexMulti(displayGroups, groupIndex, hitIndex);
|
|
11569
|
+
const extHit = hit;
|
|
11570
|
+
return (React.createElement("li", { key: getHitKey(hit, hitIndex) },
|
|
11571
|
+
React.createElement(Hit, { hit: hit, isSelected: globalIndex === selectedIndex, onClick: () => onSelect(hit), onMouseEnter: () => onHover(globalIndex), openInNewTab: group.openInNewTab, isChild: extHit.isChild, isLastChild: extHit.isLastChild, hierarchyType: hit.type })));
|
|
11572
|
+
}))))))));
|
|
11573
|
+
}
|
|
11574
|
+
|
|
11575
|
+
function Footer({ translations = {} }) {
|
|
11576
|
+
return (React.createElement("footer", { className: "seekora-docsearch-footer" },
|
|
11577
|
+
React.createElement("div", { className: "seekora-docsearch-footer-commands" },
|
|
11578
|
+
React.createElement("ul", { className: "seekora-docsearch-footer-commands-list" },
|
|
11579
|
+
React.createElement("li", null,
|
|
11580
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11581
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u21B5"),
|
|
11582
|
+
React.createElement("span", null, "to select"))),
|
|
11583
|
+
React.createElement("li", null,
|
|
11584
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11585
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2191"),
|
|
11586
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2193"),
|
|
11587
|
+
React.createElement("span", null, "to navigate"))),
|
|
11588
|
+
React.createElement("li", null,
|
|
11589
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11590
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "esc"),
|
|
11591
|
+
React.createElement("span", null, translations.closeText || 'to close'))))),
|
|
11592
|
+
React.createElement("div", { className: "seekora-docsearch-footer-logo" },
|
|
11593
|
+
React.createElement("span", { className: "seekora-docsearch-footer-logo-text" }, translations.searchByText || 'Search by'),
|
|
11594
|
+
React.createElement("a", { href: "https://seekora.ai", target: "_blank", rel: "noopener noreferrer", className: "seekora-docsearch-footer-logo-link" },
|
|
11595
|
+
React.createElement("span", { className: "seekora-docsearch-logo", style: { fontFamily: 'system-ui', fontSize: 14, fontWeight: 600 } }, "Seekora")))));
|
|
11596
|
+
}
|
|
11597
|
+
|
|
11598
|
+
function useKeyboard(options) {
|
|
11599
|
+
const { isOpen, onOpen, onClose, onSelectNext, onSelectPrev, onEnter, disableShortcut = false, shortcutKey = 'k', } = options;
|
|
11600
|
+
const handleGlobalKeyDown = useCallback((event) => {
|
|
11601
|
+
if (!disableShortcut && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey) {
|
|
11602
|
+
event.preventDefault();
|
|
11603
|
+
if (isOpen) {
|
|
11604
|
+
onClose();
|
|
11605
|
+
}
|
|
11606
|
+
else {
|
|
11607
|
+
onOpen();
|
|
11608
|
+
}
|
|
11609
|
+
return;
|
|
11610
|
+
}
|
|
11611
|
+
if (!disableShortcut && event.key === '/' && !isOpen) {
|
|
11612
|
+
const target = event.target;
|
|
11613
|
+
const isInput = target.tagName === 'INPUT' ||
|
|
11614
|
+
target.tagName === 'TEXTAREA' ||
|
|
11615
|
+
target.isContentEditable;
|
|
11616
|
+
if (!isInput) {
|
|
11617
|
+
event.preventDefault();
|
|
11618
|
+
onOpen();
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
}, [isOpen, onOpen, onClose, disableShortcut, shortcutKey]);
|
|
11622
|
+
const handleModalKeyDown = useCallback((event) => {
|
|
11623
|
+
switch (event.key) {
|
|
11624
|
+
case 'Escape':
|
|
11625
|
+
event.preventDefault();
|
|
11626
|
+
onClose();
|
|
11627
|
+
break;
|
|
11628
|
+
case 'ArrowDown':
|
|
11629
|
+
event.preventDefault();
|
|
11630
|
+
onSelectNext();
|
|
11631
|
+
break;
|
|
11632
|
+
case 'ArrowUp':
|
|
11633
|
+
event.preventDefault();
|
|
11634
|
+
onSelectPrev();
|
|
11635
|
+
break;
|
|
11636
|
+
case 'Enter':
|
|
11637
|
+
event.preventDefault();
|
|
11638
|
+
onEnter();
|
|
11639
|
+
break;
|
|
11640
|
+
case 'Tab':
|
|
11641
|
+
if (event.shiftKey) {
|
|
11642
|
+
onSelectPrev();
|
|
11643
|
+
}
|
|
11644
|
+
else {
|
|
11645
|
+
onSelectNext();
|
|
11646
|
+
}
|
|
11647
|
+
event.preventDefault();
|
|
11648
|
+
break;
|
|
11649
|
+
}
|
|
11650
|
+
}, [onClose, onSelectNext, onSelectPrev, onEnter]);
|
|
11651
|
+
useEffect(() => {
|
|
11652
|
+
document.addEventListener('keydown', handleGlobalKeyDown);
|
|
11653
|
+
return () => {
|
|
11654
|
+
document.removeEventListener('keydown', handleGlobalKeyDown);
|
|
11655
|
+
};
|
|
11656
|
+
}, [handleGlobalKeyDown]);
|
|
11657
|
+
return {
|
|
11658
|
+
handleModalKeyDown,
|
|
11659
|
+
};
|
|
11660
|
+
}
|
|
11661
|
+
function getShortcutText(key = 'K') {
|
|
11662
|
+
if (typeof navigator === 'undefined') {
|
|
11663
|
+
return `⌘${key}`;
|
|
11664
|
+
}
|
|
11665
|
+
const isMac = navigator.platform.toLowerCase().includes('mac');
|
|
11666
|
+
return isMac ? `⌘${key}` : `Ctrl+${key}`;
|
|
11667
|
+
}
|
|
11668
|
+
|
|
11669
|
+
function DocSearchButton({ onClick, placeholder = 'Search documentation...' }) {
|
|
11670
|
+
const shortcutText = getShortcutText('K');
|
|
11671
|
+
return (React.createElement("button", { type: "button", className: "seekora-docsearch-button", onClick: onClick, "aria-label": "Search documentation" },
|
|
11672
|
+
React.createElement("span", { className: "seekora-docsearch-button-icon" },
|
|
11673
|
+
React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
|
|
11674
|
+
React.createElement("path", { d: "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z", fill: "currentColor" }))),
|
|
11675
|
+
React.createElement("span", { className: "seekora-docsearch-button-placeholder" }, placeholder),
|
|
11676
|
+
React.createElement("span", { className: "seekora-docsearch-button-keys" },
|
|
11677
|
+
React.createElement("kbd", { className: "seekora-docsearch-button-key" }, shortcutText))));
|
|
11678
|
+
}
|
|
11679
|
+
|
|
11680
|
+
const initialState = {
|
|
11681
|
+
query: '',
|
|
11682
|
+
results: [],
|
|
11683
|
+
suggestions: [],
|
|
11684
|
+
groupedSuggestions: [],
|
|
11685
|
+
isLoading: false,
|
|
11686
|
+
error: null,
|
|
11687
|
+
selectedIndex: 0,
|
|
11688
|
+
mode: 'suggestions',
|
|
11689
|
+
};
|
|
11690
|
+
function reducer(state, action) {
|
|
11691
|
+
switch (action.type) {
|
|
11692
|
+
case 'SET_QUERY':
|
|
11693
|
+
return { ...state, query: action.payload, selectedIndex: 0 };
|
|
11694
|
+
case 'SET_RESULTS':
|
|
11695
|
+
return { ...state, results: action.payload, mode: 'results' };
|
|
11696
|
+
case 'SET_SUGGESTIONS':
|
|
11697
|
+
return { ...state, suggestions: action.payload, mode: 'suggestions' };
|
|
11698
|
+
case 'SET_GROUPED_SUGGESTIONS': {
|
|
11699
|
+
const flatSuggestions = action.payload.flatMap(group => group.items.map(item => ({ ...item, _source: group.source.id })));
|
|
11700
|
+
return { ...state, groupedSuggestions: action.payload, suggestions: flatSuggestions, mode: 'suggestions' };
|
|
11701
|
+
}
|
|
11702
|
+
case 'SET_LOADING':
|
|
11703
|
+
return { ...state, isLoading: action.payload };
|
|
11704
|
+
case 'SET_ERROR':
|
|
11705
|
+
return { ...state, error: action.payload };
|
|
11706
|
+
case 'SET_SELECTED_INDEX':
|
|
11707
|
+
return { ...state, selectedIndex: action.payload };
|
|
11708
|
+
case 'SELECT_NEXT': {
|
|
11709
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11710
|
+
const maxIndex = items.length - 1;
|
|
11711
|
+
return { ...state, selectedIndex: state.selectedIndex >= maxIndex ? 0 : state.selectedIndex + 1 };
|
|
11712
|
+
}
|
|
11713
|
+
case 'SELECT_PREV': {
|
|
11714
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11715
|
+
const maxIndex = items.length - 1;
|
|
11716
|
+
return { ...state, selectedIndex: state.selectedIndex <= 0 ? maxIndex : state.selectedIndex - 1 };
|
|
11717
|
+
}
|
|
11718
|
+
case 'SET_MODE':
|
|
11719
|
+
return { ...state, mode: action.payload, selectedIndex: 0 };
|
|
11720
|
+
case 'RESET':
|
|
11721
|
+
return initialState;
|
|
11722
|
+
default:
|
|
11723
|
+
return state;
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
function useDocSearch(options) {
|
|
11727
|
+
const { apiEndpoint, apiKey, sources, maxResults = 10, debounceMs = 200, processGroupedResults } = options;
|
|
11728
|
+
const searchSources = sources || (apiEndpoint ? [{
|
|
11729
|
+
id: 'default',
|
|
11730
|
+
name: 'Results',
|
|
11731
|
+
endpoint: apiEndpoint,
|
|
11732
|
+
apiKey,
|
|
11733
|
+
maxResults,
|
|
11734
|
+
}] : []);
|
|
11735
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
11736
|
+
const abortControllersRef = useRef(new Map());
|
|
11737
|
+
const debounceTimerRef = useRef(null);
|
|
11738
|
+
const defaultTransform = (data, sourceId) => {
|
|
11739
|
+
const items = data.data?.suggestions || data.data?.results || data.suggestions || data.results || data.hits || [];
|
|
11740
|
+
return items.map((item) => ({
|
|
11741
|
+
url: item.url || item.route || '',
|
|
11742
|
+
title: item.title?.replace?.(/<\/?mark>/g, '') || item.title || '',
|
|
11743
|
+
content: item.content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || item.description || '',
|
|
11744
|
+
description: item.description || item.content?.substring?.(0, 100) || '',
|
|
11745
|
+
category: item.category || item.hierarchy?.lvl0 || '',
|
|
11746
|
+
hierarchy: item.hierarchy,
|
|
11747
|
+
route: item.route,
|
|
11748
|
+
parentTitle: item.parent_title || item.parentTitle,
|
|
11749
|
+
_source: sourceId,
|
|
11750
|
+
}));
|
|
11751
|
+
};
|
|
11752
|
+
const transformPublicSearchResults = useCallback((data, sourceId) => {
|
|
11753
|
+
const results = data?.data?.results || [];
|
|
11754
|
+
return results.map((item) => {
|
|
11755
|
+
const doc = item.document || item;
|
|
11756
|
+
return {
|
|
11757
|
+
url: doc.url || doc.route || '',
|
|
11758
|
+
title: (doc.title || doc.name || '').replace?.(/<\/?mark>/g, '') || '',
|
|
11759
|
+
content: (doc.content || doc.description || '').replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || '',
|
|
11760
|
+
description: doc.description || doc.content?.substring?.(0, 100) || '',
|
|
11761
|
+
category: doc.category || doc.hierarchy?.lvl0 || '',
|
|
11762
|
+
hierarchy: doc.hierarchy,
|
|
11763
|
+
route: doc.route,
|
|
11764
|
+
parentTitle: doc.parent_title || doc.parentTitle,
|
|
11765
|
+
_source: sourceId,
|
|
11766
|
+
};
|
|
11767
|
+
});
|
|
11768
|
+
}, []);
|
|
11769
|
+
const fetchFromSource = useCallback(async (source, query, signal) => {
|
|
11770
|
+
const minLength = source.minQueryLength ?? 1;
|
|
11771
|
+
if (query.length < minLength)
|
|
11772
|
+
return [];
|
|
11773
|
+
try {
|
|
11774
|
+
if (source.storeId) {
|
|
11775
|
+
const baseUrl = source.endpoint.replace(/\/$/, '');
|
|
11776
|
+
const searchUrl = `${baseUrl}/api/v1/search`;
|
|
11777
|
+
const headers = {
|
|
11778
|
+
'Content-Type': 'application/json',
|
|
11779
|
+
'x-storeid': source.storeId,
|
|
11780
|
+
...(source.storeSecret && { 'x-storesecret': source.storeSecret }),
|
|
11781
|
+
};
|
|
11782
|
+
const response = await fetch(searchUrl, {
|
|
11783
|
+
method: 'POST',
|
|
11784
|
+
headers,
|
|
11785
|
+
body: JSON.stringify({ q: query.trim() || '*', per_page: source.maxResults || 8 }),
|
|
11786
|
+
signal,
|
|
11787
|
+
});
|
|
11788
|
+
if (!response.ok)
|
|
11789
|
+
return [];
|
|
11790
|
+
const data = await response.json();
|
|
11791
|
+
if (source.transformResults) {
|
|
11792
|
+
return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
|
|
11793
|
+
}
|
|
11794
|
+
return transformPublicSearchResults(data, source.id);
|
|
11795
|
+
}
|
|
11796
|
+
const url = new URL(source.endpoint);
|
|
11797
|
+
url.searchParams.set('query', query);
|
|
11798
|
+
url.searchParams.set('limit', String(source.maxResults || 8));
|
|
11799
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
11800
|
+
if (source.apiKey)
|
|
11801
|
+
headers['X-Docs-API-Key'] = source.apiKey;
|
|
11802
|
+
const response = await fetch(url.toString(), { method: 'GET', headers, signal });
|
|
11803
|
+
if (!response.ok)
|
|
11804
|
+
return [];
|
|
11805
|
+
const data = await response.json();
|
|
11806
|
+
if (source.transformResults) {
|
|
11807
|
+
return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
|
|
11808
|
+
}
|
|
11809
|
+
return defaultTransform(data, source.id);
|
|
11810
|
+
}
|
|
11811
|
+
catch (error) {
|
|
11812
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
11813
|
+
throw error;
|
|
11814
|
+
return [];
|
|
11815
|
+
}
|
|
11816
|
+
}, [transformPublicSearchResults]);
|
|
11817
|
+
const fetchSuggestions = useCallback(async (query) => {
|
|
11818
|
+
if (!query.trim()) {
|
|
11819
|
+
dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: [] });
|
|
11820
|
+
return;
|
|
11821
|
+
}
|
|
11822
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11823
|
+
abortControllersRef.current.clear();
|
|
11824
|
+
dispatch({ type: 'SET_LOADING', payload: true });
|
|
11825
|
+
dispatch({ type: 'SET_ERROR', payload: null });
|
|
11826
|
+
try {
|
|
11827
|
+
const results = await Promise.all(searchSources.map(async (source) => {
|
|
11828
|
+
const controller = new AbortController();
|
|
11829
|
+
abortControllersRef.current.set(source.id, controller);
|
|
11830
|
+
let items = await fetchFromSource(source, query, controller.signal);
|
|
11831
|
+
if (processGroupedResults) {
|
|
11832
|
+
items = processGroupedResults(source.id, items);
|
|
11833
|
+
}
|
|
11834
|
+
return { source, items };
|
|
11835
|
+
}));
|
|
11836
|
+
const groupedResults = results.filter(r => r.items.length > 0);
|
|
11837
|
+
dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: groupedResults });
|
|
11838
|
+
}
|
|
11839
|
+
catch (error) {
|
|
11840
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
11841
|
+
return;
|
|
11842
|
+
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Search failed' });
|
|
11843
|
+
}
|
|
11844
|
+
finally {
|
|
11845
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
11846
|
+
}
|
|
11847
|
+
}, [searchSources, fetchFromSource, processGroupedResults]);
|
|
11848
|
+
const search = useCallback((q) => fetchSuggestions(q), [fetchSuggestions]);
|
|
11849
|
+
const setQuery = useCallback((query) => {
|
|
11850
|
+
dispatch({ type: 'SET_QUERY', payload: query });
|
|
11851
|
+
if (debounceTimerRef.current)
|
|
11852
|
+
clearTimeout(debounceTimerRef.current);
|
|
11853
|
+
debounceTimerRef.current = setTimeout(() => fetchSuggestions(query), debounceMs);
|
|
11854
|
+
}, [fetchSuggestions, debounceMs]);
|
|
11855
|
+
const selectNext = useCallback(() => dispatch({ type: 'SELECT_NEXT' }), []);
|
|
11856
|
+
const selectPrev = useCallback(() => dispatch({ type: 'SELECT_PREV' }), []);
|
|
11857
|
+
const setSelectedIndex = useCallback((index) => dispatch({ type: 'SET_SELECTED_INDEX', payload: index }), []);
|
|
11858
|
+
const reset = useCallback(() => {
|
|
11859
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11860
|
+
abortControllersRef.current.clear();
|
|
11861
|
+
if (debounceTimerRef.current)
|
|
11862
|
+
clearTimeout(debounceTimerRef.current);
|
|
11863
|
+
dispatch({ type: 'RESET' });
|
|
11864
|
+
}, []);
|
|
11865
|
+
const getSelectedItem = useCallback(() => {
|
|
11866
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11867
|
+
return items[state.selectedIndex] || null;
|
|
11868
|
+
}, [state.mode, state.results, state.suggestions, state.selectedIndex]);
|
|
11869
|
+
useEffect(() => {
|
|
11870
|
+
return () => {
|
|
11871
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11872
|
+
abortControllersRef.current.clear();
|
|
11873
|
+
if (debounceTimerRef.current)
|
|
11874
|
+
clearTimeout(debounceTimerRef.current);
|
|
11875
|
+
};
|
|
11876
|
+
}, []);
|
|
11877
|
+
return {
|
|
11878
|
+
...state,
|
|
11879
|
+
sources: searchSources,
|
|
11880
|
+
setQuery,
|
|
11881
|
+
search,
|
|
11882
|
+
fetchSuggestions,
|
|
11883
|
+
selectNext,
|
|
11884
|
+
selectPrev,
|
|
11885
|
+
setSelectedIndex,
|
|
11886
|
+
reset,
|
|
11887
|
+
getSelectedItem,
|
|
11888
|
+
};
|
|
11889
|
+
}
|
|
11890
|
+
|
|
11891
|
+
/** Build hierarchy from doc: either nested doc.hierarchy or flat keys like doc['hierarchy.lvl0'] */
|
|
11892
|
+
function getHierarchy(doc) {
|
|
11893
|
+
const hierarchy = {};
|
|
11894
|
+
if (doc?.hierarchy && typeof doc.hierarchy === 'object') {
|
|
11895
|
+
hierarchy.lvl0 = doc.hierarchy.lvl0;
|
|
11896
|
+
hierarchy.lvl1 = doc.hierarchy.lvl1;
|
|
11897
|
+
hierarchy.lvl2 = doc.hierarchy.lvl2;
|
|
11898
|
+
hierarchy.lvl3 = doc.hierarchy.lvl3;
|
|
11899
|
+
hierarchy.lvl4 = doc.hierarchy.lvl4;
|
|
11900
|
+
hierarchy.lvl5 = doc.hierarchy.lvl5;
|
|
11901
|
+
}
|
|
11902
|
+
else if (doc && typeof doc === 'object') {
|
|
11903
|
+
hierarchy.lvl0 = doc['hierarchy.lvl0'];
|
|
11904
|
+
hierarchy.lvl1 = doc['hierarchy.lvl1'];
|
|
11905
|
+
hierarchy.lvl2 = doc['hierarchy.lvl2'];
|
|
11906
|
+
hierarchy.lvl3 = doc['hierarchy.lvl3'];
|
|
11907
|
+
hierarchy.lvl4 = doc['hierarchy.lvl4'];
|
|
11908
|
+
hierarchy.lvl5 = doc['hierarchy.lvl5'];
|
|
11909
|
+
}
|
|
11910
|
+
return hierarchy;
|
|
11911
|
+
}
|
|
11912
|
+
function transformResults(results) {
|
|
11913
|
+
const stripMark = (s) => typeof s === 'string' ? s.replace(/<\/?mark>/g, '') : (s ? String(s) : '');
|
|
11914
|
+
return results.map((result) => {
|
|
11915
|
+
const doc = result?.document ?? result;
|
|
11916
|
+
const hierarchy = getHierarchy(doc);
|
|
11917
|
+
const url = doc.url || result.url || doc.route || result.route || doc.link || result.link || '';
|
|
11918
|
+
const rawTitle = doc.title || result.title || doc.name || result.name || hierarchy?.lvl3 || hierarchy?.lvl2 || hierarchy?.lvl1 || hierarchy?.lvl0 || '';
|
|
11919
|
+
const content = doc.content ?? result.content ?? doc.description ?? result.description ?? doc.snippet ?? result.snippet ?? '';
|
|
11920
|
+
const description = doc.description ?? result.description ?? (typeof content === 'string' ? content.substring(0, 150) : '');
|
|
11921
|
+
const plainTitle = stripMark(rawTitle) || stripMark(hierarchy?.lvl3) || stripMark(hierarchy?.lvl2) || stripMark(hierarchy?.lvl1) || stripMark(hierarchy?.lvl0) || 'Untitled';
|
|
11922
|
+
const plainContent = stripMark(content)?.substring(0, 200) || content;
|
|
11923
|
+
const apiHighlight = result?.highlight ?? doc?.highlight;
|
|
11924
|
+
const highlightTitle = typeof apiHighlight?.title === 'string' ? apiHighlight.title : undefined;
|
|
11925
|
+
const highlightContent = typeof apiHighlight?.content === 'string' ? apiHighlight.content : undefined;
|
|
11926
|
+
return {
|
|
11927
|
+
url,
|
|
11928
|
+
title: plainTitle,
|
|
11929
|
+
content: plainContent,
|
|
11930
|
+
description: stripMark(description) || description,
|
|
11931
|
+
category: doc.category ?? result.category ?? hierarchy?.lvl0 ?? '',
|
|
11932
|
+
hierarchy,
|
|
11933
|
+
route: doc.route ?? result.route,
|
|
11934
|
+
parentTitle: doc.parent_title ?? doc.parentTitle ?? result.parent_title ?? result.parentTitle,
|
|
11935
|
+
type: doc.type ?? result.type ?? '',
|
|
11936
|
+
anchor: doc.anchor ?? result.anchor ?? '',
|
|
11937
|
+
_source: 'seekora',
|
|
11938
|
+
highlight: (highlightTitle ?? highlightContent) ? { title: highlightTitle, content: highlightContent } : undefined,
|
|
11939
|
+
};
|
|
11940
|
+
});
|
|
11941
|
+
}
|
|
11942
|
+
function useSeekoraSearch$1(options) {
|
|
11943
|
+
const { storeId, storeSecret, apiEndpoint, maxResults = 20, debounceMs = 200, analyticsTags = ['docsearch'], groupField = 'hierarchy.lvl3', groupSize = 10, } = options;
|
|
11944
|
+
const [query, setQueryState] = useState('');
|
|
11945
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
11946
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
11947
|
+
const [error, setError] = useState(null);
|
|
11948
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
11949
|
+
const clientRef = useRef(null);
|
|
11950
|
+
const lastSearchContextRef = useRef(null);
|
|
11951
|
+
const debounceTimerRef = useRef(null);
|
|
11952
|
+
const abortControllerRef = useRef(null);
|
|
11953
|
+
useEffect(() => {
|
|
11954
|
+
if (!storeId)
|
|
11955
|
+
return;
|
|
11956
|
+
const config = {
|
|
11957
|
+
storeId,
|
|
11958
|
+
readSecret: storeSecret,
|
|
11959
|
+
logLevel: 'warn',
|
|
11960
|
+
enableContextCollection: true,
|
|
11961
|
+
};
|
|
11962
|
+
if (apiEndpoint) {
|
|
11963
|
+
if (['local', 'stage', 'production'].includes(apiEndpoint)) {
|
|
11964
|
+
config.environment = apiEndpoint;
|
|
11965
|
+
}
|
|
11966
|
+
else {
|
|
11967
|
+
// Search SDK path is /v1/search; backend serves at /api/v1/search. baseUrl must include /api.
|
|
11968
|
+
const base = apiEndpoint.replace(/\/$/, '');
|
|
11969
|
+
config.baseUrl = base.endsWith('/api') ? base : `${base}/api`;
|
|
11970
|
+
}
|
|
11971
|
+
}
|
|
11972
|
+
try {
|
|
11973
|
+
clientRef.current = new SeekoraClient(config);
|
|
11974
|
+
}
|
|
11975
|
+
catch (err) {
|
|
11976
|
+
console.error('Failed to initialize SeekoraClient:', err);
|
|
11977
|
+
setError('Failed to initialize search client');
|
|
11978
|
+
}
|
|
11979
|
+
return () => {
|
|
11980
|
+
clientRef.current = null;
|
|
11981
|
+
};
|
|
11982
|
+
}, [storeId, storeSecret, apiEndpoint]);
|
|
11983
|
+
const performSearch = useCallback(async (searchQuery) => {
|
|
11984
|
+
if (!clientRef.current) {
|
|
11985
|
+
setError('Search client not initialized');
|
|
11986
|
+
return;
|
|
11987
|
+
}
|
|
11988
|
+
if (!searchQuery.trim()) {
|
|
11989
|
+
setSuggestions([]);
|
|
11990
|
+
return;
|
|
11991
|
+
}
|
|
11992
|
+
if (abortControllerRef.current)
|
|
11993
|
+
abortControllerRef.current.abort();
|
|
11994
|
+
abortControllerRef.current = new AbortController();
|
|
11995
|
+
setIsLoading(true);
|
|
11996
|
+
setError(null);
|
|
11997
|
+
try {
|
|
11998
|
+
const response = await clientRef.current.search(searchQuery, {
|
|
11999
|
+
per_page: maxResults,
|
|
12000
|
+
analytics_tags: analyticsTags,
|
|
12001
|
+
return_fields: [
|
|
12002
|
+
'hierarchy.lvl0', 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
|
|
12003
|
+
'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6',
|
|
12004
|
+
'content', 'type', 'url', 'title', 'anchor'
|
|
12005
|
+
],
|
|
12006
|
+
snippet_fields: [
|
|
12007
|
+
'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
|
|
12008
|
+
'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6', 'content'
|
|
12009
|
+
],
|
|
12010
|
+
snippet_prefix: '<mark>',
|
|
12011
|
+
snippet_suffix: '</mark>',
|
|
12012
|
+
include_snippets: true,
|
|
12013
|
+
group_field: groupField,
|
|
12014
|
+
group_size: groupSize,
|
|
12015
|
+
});
|
|
12016
|
+
if (abortControllerRef.current?.signal.aborted)
|
|
12017
|
+
return;
|
|
12018
|
+
if (response?.context)
|
|
12019
|
+
lastSearchContextRef.current = response.context;
|
|
12020
|
+
setSuggestions(transformResults(response.results || []));
|
|
12021
|
+
setSelectedIndex(0);
|
|
12022
|
+
}
|
|
12023
|
+
catch (err) {
|
|
12024
|
+
if (err instanceof Error && err.name === 'AbortError')
|
|
12025
|
+
return;
|
|
12026
|
+
console.error('Search failed:', err);
|
|
12027
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
12028
|
+
setSuggestions([]);
|
|
12029
|
+
}
|
|
12030
|
+
finally {
|
|
12031
|
+
setIsLoading(false);
|
|
12032
|
+
}
|
|
12033
|
+
}, [maxResults, analyticsTags, groupField, groupSize]);
|
|
12034
|
+
const setQuery = useCallback((newQuery) => {
|
|
12035
|
+
setQueryState(newQuery);
|
|
12036
|
+
setSelectedIndex(0);
|
|
12037
|
+
if (debounceTimerRef.current)
|
|
12038
|
+
clearTimeout(debounceTimerRef.current);
|
|
12039
|
+
debounceTimerRef.current = setTimeout(() => performSearch(newQuery), debounceMs);
|
|
12040
|
+
}, [performSearch, debounceMs]);
|
|
12041
|
+
const selectNext = useCallback(() => {
|
|
12042
|
+
setSelectedIndex((prev) => (prev >= suggestions.length - 1 ? 0 : prev + 1));
|
|
12043
|
+
}, [suggestions.length]);
|
|
12044
|
+
const selectPrev = useCallback(() => {
|
|
12045
|
+
setSelectedIndex((prev) => (prev <= 0 ? suggestions.length - 1 : prev - 1));
|
|
12046
|
+
}, [suggestions.length]);
|
|
12047
|
+
const reset = useCallback(() => {
|
|
12048
|
+
if (abortControllerRef.current)
|
|
12049
|
+
abortControllerRef.current.abort();
|
|
12050
|
+
if (debounceTimerRef.current)
|
|
12051
|
+
clearTimeout(debounceTimerRef.current);
|
|
12052
|
+
setQueryState('');
|
|
12053
|
+
setSuggestions([]);
|
|
12054
|
+
setIsLoading(false);
|
|
12055
|
+
setError(null);
|
|
12056
|
+
setSelectedIndex(0);
|
|
12057
|
+
}, []);
|
|
12058
|
+
const getSelectedItem = useCallback(() => {
|
|
12059
|
+
return suggestions[selectedIndex] || null;
|
|
12060
|
+
}, [suggestions, selectedIndex]);
|
|
12061
|
+
const trackDocClick = useCallback((hit, position) => {
|
|
12062
|
+
const client = clientRef.current;
|
|
12063
|
+
if (!client?.trackEvent)
|
|
12064
|
+
return;
|
|
12065
|
+
const context = lastSearchContextRef.current ?? undefined;
|
|
12066
|
+
const itemId = hit.url || hit.id || hit.title || String(position);
|
|
12067
|
+
client.trackEvent({
|
|
12068
|
+
event_name: 'doc_click',
|
|
12069
|
+
clicked_item_id: itemId,
|
|
12070
|
+
metadata: { position, result: hit, source: 'docsearch' },
|
|
12071
|
+
}, context);
|
|
12072
|
+
}, []);
|
|
12073
|
+
useEffect(() => {
|
|
12074
|
+
return () => {
|
|
12075
|
+
if (abortControllerRef.current)
|
|
12076
|
+
abortControllerRef.current.abort();
|
|
12077
|
+
if (debounceTimerRef.current)
|
|
12078
|
+
clearTimeout(debounceTimerRef.current);
|
|
12079
|
+
};
|
|
12080
|
+
}, []);
|
|
12081
|
+
return {
|
|
12082
|
+
query,
|
|
12083
|
+
suggestions,
|
|
12084
|
+
isLoading,
|
|
12085
|
+
error,
|
|
12086
|
+
selectedIndex,
|
|
12087
|
+
setQuery,
|
|
12088
|
+
selectNext,
|
|
12089
|
+
selectPrev,
|
|
12090
|
+
setSelectedIndex,
|
|
12091
|
+
reset,
|
|
12092
|
+
getSelectedItem,
|
|
12093
|
+
trackDocClick,
|
|
12094
|
+
};
|
|
12095
|
+
}
|
|
12096
|
+
|
|
12097
|
+
function DocSearch({ storeId, storeSecret, seekoraApiEndpoint, apiEndpoint, apiKey, sources, placeholder = 'Search documentation...', maxResults = 10, debounceMs = 200, onSelect, onClose, translations = {}, renderButton = true, buttonComponent: ButtonComponent = DocSearchButton, initialOpen = false, disableShortcut = false, shortcutKey = 'k', processGroupedResults, }) {
|
|
12098
|
+
const [isOpen, setIsOpen] = useState(initialOpen);
|
|
12099
|
+
const scrollSelectionIntoViewRef = useRef(false);
|
|
12100
|
+
const useSeekoraSDK = !!storeId;
|
|
12101
|
+
const seekoraSearch = useSeekoraSearch$1({
|
|
12102
|
+
storeId: storeId || '',
|
|
12103
|
+
storeSecret,
|
|
12104
|
+
apiEndpoint: seekoraApiEndpoint,
|
|
12105
|
+
maxResults,
|
|
12106
|
+
debounceMs,
|
|
12107
|
+
analyticsTags: ['docsearch'],
|
|
12108
|
+
});
|
|
12109
|
+
const legacySearch = useDocSearch({
|
|
12110
|
+
apiEndpoint,
|
|
12111
|
+
apiKey,
|
|
12112
|
+
sources,
|
|
12113
|
+
maxResults,
|
|
12114
|
+
debounceMs,
|
|
12115
|
+
processGroupedResults,
|
|
12116
|
+
});
|
|
12117
|
+
const { query, suggestions, isLoading, error, selectedIndex, setQuery, selectNext, selectPrev, setSelectedIndex, reset, getSelectedItem, } = useSeekoraSDK ? seekoraSearch : legacySearch;
|
|
12118
|
+
const groupedSuggestions = useSeekoraSDK ? undefined : legacySearch.groupedSuggestions;
|
|
12119
|
+
const results = useSeekoraSDK ? suggestions : legacySearch.results;
|
|
12120
|
+
const mode = useSeekoraSDK ? 'suggestions' : legacySearch.mode;
|
|
12121
|
+
const searchSources = useSeekoraSDK
|
|
12122
|
+
? [{ id: 'seekora', name: 'Results', endpoint: '' }]
|
|
12123
|
+
: legacySearch.sources;
|
|
12124
|
+
const handleOpen = useCallback(() => setIsOpen(true), []);
|
|
12125
|
+
const handleClose = useCallback(() => {
|
|
12126
|
+
setIsOpen(false);
|
|
12127
|
+
reset();
|
|
12128
|
+
onClose?.();
|
|
12129
|
+
}, [reset, onClose]);
|
|
12130
|
+
const handleSelect = useCallback((hit) => {
|
|
12131
|
+
if (useSeekoraSDK && seekoraSearch.trackDocClick) {
|
|
12132
|
+
seekoraSearch.trackDocClick(hit, selectedIndex + 1);
|
|
12133
|
+
}
|
|
12134
|
+
if (onSelect) {
|
|
12135
|
+
onSelect(hit);
|
|
12136
|
+
}
|
|
12137
|
+
else {
|
|
12138
|
+
window.location.href = hit.url;
|
|
12139
|
+
}
|
|
12140
|
+
handleClose();
|
|
12141
|
+
}, [onSelect, handleClose, useSeekoraSDK, seekoraSearch, selectedIndex]);
|
|
12142
|
+
const handleEnter = useCallback(() => {
|
|
12143
|
+
const selectedItem = getSelectedItem();
|
|
12144
|
+
if (selectedItem)
|
|
12145
|
+
handleSelect(selectedItem);
|
|
12146
|
+
}, [getSelectedItem, handleSelect]);
|
|
12147
|
+
const handleSelectNext = useCallback(() => {
|
|
12148
|
+
scrollSelectionIntoViewRef.current = true;
|
|
12149
|
+
selectNext();
|
|
12150
|
+
}, [selectNext]);
|
|
12151
|
+
const handleSelectPrev = useCallback(() => {
|
|
12152
|
+
scrollSelectionIntoViewRef.current = true;
|
|
12153
|
+
selectPrev();
|
|
12154
|
+
}, [selectPrev]);
|
|
12155
|
+
const { handleModalKeyDown } = useKeyboard({
|
|
12156
|
+
isOpen,
|
|
12157
|
+
onOpen: handleOpen,
|
|
12158
|
+
onClose: handleClose,
|
|
12159
|
+
onSelectNext: handleSelectNext,
|
|
12160
|
+
onSelectPrev: handleSelectPrev,
|
|
12161
|
+
onEnter: handleEnter,
|
|
12162
|
+
disableShortcut,
|
|
12163
|
+
shortcutKey,
|
|
12164
|
+
});
|
|
12165
|
+
const handleKeyDown = useCallback((event) => handleModalKeyDown(event), [handleModalKeyDown]);
|
|
12166
|
+
const displayHits = mode === 'results' ? results : suggestions;
|
|
12167
|
+
return (React.createElement(React.Fragment, null,
|
|
12168
|
+
renderButton && (React.createElement(ButtonComponent, { onClick: handleOpen, placeholder: translations.buttonText || placeholder })),
|
|
12169
|
+
React.createElement(Modal, { isOpen: isOpen, onClose: handleClose },
|
|
12170
|
+
React.createElement("div", { className: "seekora-docsearch-modal", onKeyDown: handleKeyDown },
|
|
12171
|
+
React.createElement("header", { className: "seekora-docsearch-header" },
|
|
12172
|
+
React.createElement(SearchBox, { value: query, onChange: setQuery, onKeyDown: handleKeyDown, placeholder: placeholder, isLoading: isLoading, onClear: reset }),
|
|
12173
|
+
React.createElement("button", { type: "button", className: "seekora-docsearch-close", onClick: handleClose, "aria-label": "Close search" },
|
|
12174
|
+
React.createElement("span", { className: "seekora-docsearch-close-text" }, "esc"))),
|
|
12175
|
+
React.createElement("div", { className: "seekora-docsearch-body" },
|
|
12176
|
+
React.createElement(Results, { hits: displayHits, groupedHits: groupedSuggestions, selectedIndex: selectedIndex, onSelect: handleSelect, onHover: setSelectedIndex, scrollSelectionIntoViewRef: scrollSelectionIntoViewRef, query: query, isLoading: isLoading, error: error, translations: translations, sources: searchSources })),
|
|
12177
|
+
React.createElement(Footer, { translations: translations })))));
|
|
12178
|
+
}
|
|
12179
|
+
|
|
10564
12180
|
/**
|
|
10565
12181
|
* useSeekoraSearch Hook
|
|
10566
12182
|
*
|
|
@@ -10626,7 +12242,9 @@ const useSeekoraSearch = ({ client, autoTrack = true, }) => {
|
|
|
10626
12242
|
/**
|
|
10627
12243
|
* useAnalytics Hook
|
|
10628
12244
|
*
|
|
10629
|
-
* Hook for tracking analytics events with the Seekora SDK
|
|
12245
|
+
* Hook for tracking analytics events with the Seekora SDK.
|
|
12246
|
+
* Supports Analytics V3 payload fields (event_ts, anonymous_id, orgcode, xstoreid);
|
|
12247
|
+
* the SDK sends both legacy and v3 fields for backend compatibility.
|
|
10630
12248
|
*/
|
|
10631
12249
|
const useAnalytics = ({ client, enabled = true, }) => {
|
|
10632
12250
|
const trackEvent = useCallback(async (eventType, payload, context) => {
|
|
@@ -10646,14 +12264,17 @@ const useAnalytics = ({ client, enabled = true, }) => {
|
|
|
10646
12264
|
const trackClick = useCallback(async (resultId, result, context, position) => {
|
|
10647
12265
|
if (!enabled)
|
|
10648
12266
|
return;
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
12267
|
+
const pos = position ?? 0;
|
|
12268
|
+
if (client.trackClick) {
|
|
12269
|
+
await client.trackClick(resultId, pos, context);
|
|
12270
|
+
}
|
|
12271
|
+
else {
|
|
12272
|
+
await trackEvent('product_click', {
|
|
12273
|
+
clicked_item_id: resultId,
|
|
12274
|
+
metadata: { result, ...(position !== undefined && { position: pos }) },
|
|
12275
|
+
}, context);
|
|
12276
|
+
}
|
|
12277
|
+
}, [client, trackEvent, enabled]);
|
|
10657
12278
|
const trackConversion = useCallback(async (resultId, result, value, currency, context) => {
|
|
10658
12279
|
if (!enabled)
|
|
10659
12280
|
return;
|
|
@@ -11675,5 +13296,5 @@ function updateSuggestionsStyles(theme) {
|
|
|
11675
13296
|
injectSuggestionsStyles(theme, true);
|
|
11676
13297
|
}
|
|
11677
13298
|
|
|
11678
|
-
export { AmazonDropdown, Breadcrumb, ClearRefinements, CurrentRefinements, Facets, FederatedDropdown, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight, HitsPerPage, InfiniteHits, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchLayout, SearchProvider, SearchResults, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionSearchBar, TrendingItems, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBrand, extractCategory, extractProduct, extractSuggestion, formatParsedFilters, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getRecentSearches, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useInjectResponsiveStyles, useNaturalLanguageFilters, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics };
|
|
13299
|
+
export { AmazonDropdown, Breadcrumb, ClearRefinements, CurrentRefinements, DocSearch, DocSearchButton, Facets, FederatedDropdown, Fingerprint, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight$1 as Highlight, HitsPerPage, InfiniteHits, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchLayout, SearchProvider, SearchResults, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionSearchBar, TrendingItems, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBrand, extractCategory, extractProduct, extractSuggestion, formatParsedFilters, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getFingerprint, getRecentSearches, getShortcutText, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useDocSearch, useSeekoraSearch$1 as useDocSearchSeekoraSearch, useInjectResponsiveStyles, useKeyboard, useNaturalLanguageFilters, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics };
|
|
11679
13300
|
//# sourceMappingURL=index.esm.js.map
|