@seekora-ai/ui-sdk-react 0.1.1 → 0.2.4
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/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 +81 -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 +21 -0
- package/dist/docsearch/components/Results.d.ts.map +1 -0
- package/dist/docsearch/components/Results.js +91 -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 +32 -0
- package/dist/docsearch/hooks/useDocSearch.d.ts.map +1 -0
- package/dist/docsearch/hooks/useDocSearch.js +208 -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 +187 -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 +170 -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/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 +258 -7
- package/dist/src/index.esm.js +1611 -79
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +1618 -78
- package/dist/src/index.js.map +1 -1
- package/package.json +8 -6
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
|
|
@@ -5587,7 +6237,7 @@ const EVENTS = {
|
|
|
5587
6237
|
// Hook Implementation
|
|
5588
6238
|
// ============================================================================
|
|
5589
6239
|
function useSuggestionsAnalytics(options) {
|
|
5590
|
-
const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, } = options;
|
|
6240
|
+
const { client, enabled = true, analyticsTags = [], impressionDebounce = 500, trackImpressions = true, trackClicks = true, context: contextOption, } = options;
|
|
5591
6241
|
// Refs for debouncing and tracking
|
|
5592
6242
|
const impressionTimerRef = useRef(null);
|
|
5593
6243
|
const lastImpressionRef = useRef(null);
|
|
@@ -5600,10 +6250,11 @@ function useSuggestionsAnalytics(options) {
|
|
|
5600
6250
|
}
|
|
5601
6251
|
};
|
|
5602
6252
|
}, []);
|
|
5603
|
-
// Helper to send event
|
|
5604
|
-
const sendEvent = useCallback(async (eventName, metadata) => {
|
|
6253
|
+
// Helper to send event (optional context links event to search for v3 analytics)
|
|
6254
|
+
const sendEvent = useCallback(async (eventName, metadata, context) => {
|
|
5605
6255
|
if (!enabled || !client)
|
|
5606
6256
|
return;
|
|
6257
|
+
const searchContext = context ?? contextOption;
|
|
5607
6258
|
try {
|
|
5608
6259
|
await client.trackEvent?.({
|
|
5609
6260
|
event_name: eventName,
|
|
@@ -5613,13 +6264,13 @@ function useSuggestionsAnalytics(options) {
|
|
|
5613
6264
|
timestamp: Date.now(),
|
|
5614
6265
|
source: 'suggestions_dropdown',
|
|
5615
6266
|
},
|
|
5616
|
-
});
|
|
6267
|
+
}, searchContext);
|
|
5617
6268
|
log.verbose(`Analytics: ${eventName}`, metadata);
|
|
5618
6269
|
}
|
|
5619
6270
|
catch (error) {
|
|
5620
6271
|
log.warn(`Failed to track ${eventName}`, { error });
|
|
5621
6272
|
}
|
|
5622
|
-
}, [client, enabled, analyticsTags]);
|
|
6273
|
+
}, [client, enabled, analyticsTags, contextOption]);
|
|
5623
6274
|
// Track suggestion click
|
|
5624
6275
|
const trackSuggestionClick = useCallback((data) => {
|
|
5625
6276
|
if (!trackClicks)
|
|
@@ -5647,11 +6298,12 @@ function useSuggestionsAnalytics(options) {
|
|
|
5647
6298
|
tab_id: data.tabId,
|
|
5648
6299
|
original_query: data.query,
|
|
5649
6300
|
});
|
|
5650
|
-
// Also track as a general product click for analytics
|
|
5651
|
-
|
|
5652
|
-
|
|
6301
|
+
// Also track as a general product click for analytics (with context when available)
|
|
6302
|
+
const searchContext = contextOption;
|
|
6303
|
+
if (client?.trackClick) {
|
|
6304
|
+
Promise.resolve(client.trackClick(data.product.id || data.product.objectID || '', data.position + 1, searchContext)).catch(() => { });
|
|
5653
6305
|
}
|
|
5654
|
-
}, [client, sendEvent, trackClicks]);
|
|
6306
|
+
}, [client, contextOption, sendEvent, trackClicks]);
|
|
5655
6307
|
// Track category click
|
|
5656
6308
|
const trackCategoryClick = useCallback((category, query) => {
|
|
5657
6309
|
if (!trackClicks)
|
|
@@ -10561,6 +11213,881 @@ const SuggestionDropdownVariants = {
|
|
|
10561
11213
|
minimal: MinimalDropdown,
|
|
10562
11214
|
};
|
|
10563
11215
|
|
|
11216
|
+
function Modal({ isOpen, onClose, children }) {
|
|
11217
|
+
const overlayRef = useRef(null);
|
|
11218
|
+
const containerRef = useRef(null);
|
|
11219
|
+
useEffect(() => {
|
|
11220
|
+
const handleClickOutside = (event) => {
|
|
11221
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
11222
|
+
onClose();
|
|
11223
|
+
}
|
|
11224
|
+
};
|
|
11225
|
+
if (isOpen)
|
|
11226
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
11227
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
11228
|
+
}, [isOpen, onClose]);
|
|
11229
|
+
useEffect(() => {
|
|
11230
|
+
if (isOpen) {
|
|
11231
|
+
const originalOverflow = document.body.style.overflow;
|
|
11232
|
+
document.body.style.overflow = 'hidden';
|
|
11233
|
+
return () => { document.body.style.overflow = originalOverflow; };
|
|
11234
|
+
}
|
|
11235
|
+
}, [isOpen]);
|
|
11236
|
+
useEffect(() => {
|
|
11237
|
+
if (!isOpen || !containerRef.current)
|
|
11238
|
+
return;
|
|
11239
|
+
const container = containerRef.current;
|
|
11240
|
+
const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
11241
|
+
const firstElement = focusableElements[0];
|
|
11242
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
11243
|
+
const handleTabKey = (e) => {
|
|
11244
|
+
if (e.key !== 'Tab')
|
|
11245
|
+
return;
|
|
11246
|
+
if (e.shiftKey) {
|
|
11247
|
+
if (document.activeElement === firstElement) {
|
|
11248
|
+
e.preventDefault();
|
|
11249
|
+
lastElement?.focus();
|
|
11250
|
+
}
|
|
11251
|
+
}
|
|
11252
|
+
else {
|
|
11253
|
+
if (document.activeElement === lastElement) {
|
|
11254
|
+
e.preventDefault();
|
|
11255
|
+
firstElement?.focus();
|
|
11256
|
+
}
|
|
11257
|
+
}
|
|
11258
|
+
};
|
|
11259
|
+
container.addEventListener('keydown', handleTabKey);
|
|
11260
|
+
return () => container.removeEventListener('keydown', handleTabKey);
|
|
11261
|
+
}, [isOpen]);
|
|
11262
|
+
if (!isOpen || typeof document === 'undefined')
|
|
11263
|
+
return null;
|
|
11264
|
+
const modalContent = (React.createElement("div", { ref: overlayRef, className: "seekora-docsearch-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Search documentation" },
|
|
11265
|
+
React.createElement("div", { ref: containerRef, className: "seekora-docsearch-container" }, children)));
|
|
11266
|
+
return createPortal(modalContent, document.body);
|
|
11267
|
+
}
|
|
11268
|
+
|
|
11269
|
+
function SearchBox({ value, onChange, onKeyDown, placeholder = 'Search documentation...', isLoading = false, onClear, }) {
|
|
11270
|
+
const inputRef = useRef(null);
|
|
11271
|
+
useEffect(() => { if (inputRef.current)
|
|
11272
|
+
inputRef.current.focus(); }, []);
|
|
11273
|
+
const handleChange = (event) => onChange(event.target.value);
|
|
11274
|
+
const handleClear = () => { onChange(''); onClear?.(); inputRef.current?.focus(); };
|
|
11275
|
+
return (React.createElement("div", { className: "seekora-docsearch-searchbox" },
|
|
11276
|
+
React.createElement("label", { className: "seekora-docsearch-searchbox-icon", htmlFor: "seekora-docsearch-input" }, isLoading ? (React.createElement("span", { className: "seekora-docsearch-spinner", "aria-hidden": "true" },
|
|
11277
|
+
React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20" },
|
|
11278
|
+
React.createElement("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "40", strokeDashoffset: "10" },
|
|
11279
|
+
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" },
|
|
11280
|
+
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" })))),
|
|
11281
|
+
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" }),
|
|
11282
|
+
value && (React.createElement("button", { type: "button", className: "seekora-docsearch-clear", onClick: handleClear, "aria-label": "Clear search" },
|
|
11283
|
+
React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
|
|
11284
|
+
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" }))))));
|
|
11285
|
+
}
|
|
11286
|
+
|
|
11287
|
+
function sanitizeHtml(html) {
|
|
11288
|
+
const escaped = html
|
|
11289
|
+
.replace(/&/g, '&')
|
|
11290
|
+
.replace(/</g, '<')
|
|
11291
|
+
.replace(/>/g, '>')
|
|
11292
|
+
.replace(/"/g, '"')
|
|
11293
|
+
.replace(/'/g, ''');
|
|
11294
|
+
return escaped
|
|
11295
|
+
.replace(/<mark>/g, '<mark>')
|
|
11296
|
+
.replace(/<\/mark>/g, '</mark>')
|
|
11297
|
+
.replace(/<ais-highlight>/g, '<mark>')
|
|
11298
|
+
.replace(/<\/ais-highlight>/g, '</mark>')
|
|
11299
|
+
.replace(/<em>/g, '<mark>')
|
|
11300
|
+
.replace(/<\/em>/g, '</mark>');
|
|
11301
|
+
}
|
|
11302
|
+
function Highlight({ value, highlightedValue }) {
|
|
11303
|
+
if (!highlightedValue)
|
|
11304
|
+
return React.createElement("span", null, value);
|
|
11305
|
+
return (React.createElement("span", { className: "seekora-docsearch-highlight", dangerouslySetInnerHTML: { __html: sanitizeHtml(highlightedValue) } }));
|
|
11306
|
+
}
|
|
11307
|
+
function truncateAroundMatch(content, maxLength = 150) {
|
|
11308
|
+
const markIndex = content.indexOf('<mark>');
|
|
11309
|
+
if (markIndex === -1 || content.length <= maxLength) {
|
|
11310
|
+
if (content.length <= maxLength)
|
|
11311
|
+
return content;
|
|
11312
|
+
return content.slice(0, maxLength) + '...';
|
|
11313
|
+
}
|
|
11314
|
+
const halfLength = Math.floor(maxLength / 2);
|
|
11315
|
+
let start = Math.max(0, markIndex - halfLength);
|
|
11316
|
+
let end = Math.min(content.length, markIndex + halfLength);
|
|
11317
|
+
if (start > 0) {
|
|
11318
|
+
const spaceIndex = content.indexOf(' ', start);
|
|
11319
|
+
if (spaceIndex !== -1 && spaceIndex < markIndex)
|
|
11320
|
+
start = spaceIndex + 1;
|
|
11321
|
+
}
|
|
11322
|
+
if (end < content.length) {
|
|
11323
|
+
const spaceIndex = content.lastIndexOf(' ', end);
|
|
11324
|
+
if (spaceIndex !== -1 && spaceIndex > markIndex)
|
|
11325
|
+
end = spaceIndex;
|
|
11326
|
+
}
|
|
11327
|
+
let result = content.slice(start, end);
|
|
11328
|
+
if (start > 0)
|
|
11329
|
+
result = '...' + result;
|
|
11330
|
+
if (end < content.length)
|
|
11331
|
+
result = result + '...';
|
|
11332
|
+
return result;
|
|
11333
|
+
}
|
|
11334
|
+
|
|
11335
|
+
function Hit({ hit, isSelected, onClick, onMouseEnter, openInNewTab, isChild, isLastChild, hierarchyType }) {
|
|
11336
|
+
const isFullHit = 'objectID' in hit;
|
|
11337
|
+
const suggestion = hit;
|
|
11338
|
+
const hitType = hierarchyType || suggestion.type;
|
|
11339
|
+
const breadcrumb = suggestion.parentTitle
|
|
11340
|
+
? `${suggestion.category || ''} › ${suggestion.parentTitle}`.replace(/^› /, '')
|
|
11341
|
+
: suggestion.category || '';
|
|
11342
|
+
const title = getTitleForType(hit, hitType);
|
|
11343
|
+
let highlightedTitle = title;
|
|
11344
|
+
let highlightedContent = hit.content || suggestion.description || '';
|
|
11345
|
+
if (isFullHit) {
|
|
11346
|
+
const fullHit = hit;
|
|
11347
|
+
if (fullHit._highlightResult) {
|
|
11348
|
+
highlightedTitle = fullHit._highlightResult.title?.value || title;
|
|
11349
|
+
highlightedContent = fullHit._highlightResult.content?.value || hit.content || '';
|
|
11350
|
+
}
|
|
11351
|
+
}
|
|
11352
|
+
else {
|
|
11353
|
+
if (suggestion.highlight) {
|
|
11354
|
+
highlightedTitle = suggestion.highlight.title || title;
|
|
11355
|
+
highlightedContent = suggestion.highlight.content || hit.content || suggestion.description || '';
|
|
11356
|
+
}
|
|
11357
|
+
}
|
|
11358
|
+
const displayContent = highlightedContent ? truncateAroundMatch(highlightedContent, 120) : '';
|
|
11359
|
+
const url = hit.url || suggestion.route || '#';
|
|
11360
|
+
const classNames = ['seekora-docsearch-hit'];
|
|
11361
|
+
if (isSelected)
|
|
11362
|
+
classNames.push('seekora-docsearch-hit--selected');
|
|
11363
|
+
if (isChild)
|
|
11364
|
+
classNames.push('seekora-docsearch-hit--child');
|
|
11365
|
+
if (isLastChild)
|
|
11366
|
+
classNames.push('seekora-docsearch-hit--last-child');
|
|
11367
|
+
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 },
|
|
11368
|
+
isChild && (React.createElement("div", { className: "seekora-docsearch-hit-tree" },
|
|
11369
|
+
React.createElement(TreeConnector, { isLast: isLastChild }))),
|
|
11370
|
+
React.createElement("div", { className: "seekora-docsearch-hit-icon" },
|
|
11371
|
+
React.createElement(HitIcon, { type: getHitTypeFromLevel(hitType) })),
|
|
11372
|
+
React.createElement("div", { className: "seekora-docsearch-hit-content" },
|
|
11373
|
+
!isChild && breadcrumb && React.createElement("span", { className: "seekora-docsearch-hit-breadcrumb" }, breadcrumb),
|
|
11374
|
+
React.createElement("span", { className: "seekora-docsearch-hit-title" },
|
|
11375
|
+
React.createElement(Highlight, { value: title, highlightedValue: highlightedTitle })),
|
|
11376
|
+
displayContent && (React.createElement("span", { className: "seekora-docsearch-hit-description" },
|
|
11377
|
+
React.createElement(Highlight, { value: hit.content || '', highlightedValue: displayContent })))),
|
|
11378
|
+
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" },
|
|
11379
|
+
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" },
|
|
11380
|
+
React.createElement("path", { d: "M6.75 3.25L11.5 8L6.75 12.75", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))))));
|
|
11381
|
+
}
|
|
11382
|
+
function getTitleForType(hit, type) {
|
|
11383
|
+
const hierarchy = hit.hierarchy || {};
|
|
11384
|
+
if (!type)
|
|
11385
|
+
return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
|
|
11386
|
+
const match = type.match(/^lvl(\d+)$/);
|
|
11387
|
+
if (match) {
|
|
11388
|
+
const level = parseInt(match[1], 10);
|
|
11389
|
+
const levelKey = `lvl${level}`;
|
|
11390
|
+
const levelTitle = hierarchy[levelKey];
|
|
11391
|
+
if (levelTitle)
|
|
11392
|
+
return levelTitle;
|
|
11393
|
+
}
|
|
11394
|
+
return hit.title || hierarchy.lvl1 || hierarchy.lvl0 || 'Untitled';
|
|
11395
|
+
}
|
|
11396
|
+
function getHitTypeFromLevel(type) {
|
|
11397
|
+
if (!type)
|
|
11398
|
+
return 'page';
|
|
11399
|
+
const match = type.match(/^lvl(\d+)$/);
|
|
11400
|
+
if (match) {
|
|
11401
|
+
const level = parseInt(match[1], 10);
|
|
11402
|
+
if (level === 1)
|
|
11403
|
+
return 'page';
|
|
11404
|
+
if (level <= 3)
|
|
11405
|
+
return 'section';
|
|
11406
|
+
return 'content';
|
|
11407
|
+
}
|
|
11408
|
+
return 'page';
|
|
11409
|
+
}
|
|
11410
|
+
function HitIcon({ type }) {
|
|
11411
|
+
switch (type) {
|
|
11412
|
+
case 'page':
|
|
11413
|
+
return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
|
|
11414
|
+
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" }),
|
|
11415
|
+
React.createElement("path", { d: "M7 7h6M7 10h6M7 13h4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
|
|
11416
|
+
case 'section':
|
|
11417
|
+
return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
|
|
11418
|
+
React.createElement("path", { d: "M4 5.5h12M4 10h12M4 14.5h8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
|
|
11419
|
+
case 'content':
|
|
11420
|
+
return (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true" },
|
|
11421
|
+
React.createElement("path", { d: "M4 6h12M4 10h8M4 14h10", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })));
|
|
11422
|
+
}
|
|
11423
|
+
}
|
|
11424
|
+
function TreeConnector({ isLast }) {
|
|
11425
|
+
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,
|
|
11426
|
+
React.createElement("path", { d: "M8 0V20", stroke: "currentColor", strokeWidth: "1.5" }),
|
|
11427
|
+
React.createElement("path", { d: "M8 10H14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })))));
|
|
11428
|
+
}
|
|
11429
|
+
|
|
11430
|
+
function getTypeLevel(type) {
|
|
11431
|
+
if (!type)
|
|
11432
|
+
return 1;
|
|
11433
|
+
const match = type.match(/^lvl(\d+)$/);
|
|
11434
|
+
return match ? parseInt(match[1], 10) : 1;
|
|
11435
|
+
}
|
|
11436
|
+
function isChildType(type) {
|
|
11437
|
+
return getTypeLevel(type) >= 2;
|
|
11438
|
+
}
|
|
11439
|
+
function groupHitsByHierarchy(hits) {
|
|
11440
|
+
const groups = new Map();
|
|
11441
|
+
for (const hit of hits) {
|
|
11442
|
+
const lvl0 = hit.hierarchy?.lvl0 || '';
|
|
11443
|
+
if (!groups.has(lvl0))
|
|
11444
|
+
groups.set(lvl0, []);
|
|
11445
|
+
groups.get(lvl0).push(hit);
|
|
11446
|
+
}
|
|
11447
|
+
const result = [];
|
|
11448
|
+
for (const [lvl0Key, groupHits] of groups.entries()) {
|
|
11449
|
+
const sortedHits = [...groupHits].sort((a, b) => {
|
|
11450
|
+
const aType = a.type;
|
|
11451
|
+
const bType = b.type;
|
|
11452
|
+
return getTypeLevel(aType) - getTypeLevel(bType);
|
|
11453
|
+
});
|
|
11454
|
+
const markedHits = sortedHits.map((hit, index) => {
|
|
11455
|
+
const suggestion = hit;
|
|
11456
|
+
const isChild = isChildType(suggestion.type);
|
|
11457
|
+
const nextHit = sortedHits[index + 1];
|
|
11458
|
+
const isLastChild = isChild && (!nextHit || !isChildType(nextHit.type));
|
|
11459
|
+
return { ...hit, isChild, isLastChild };
|
|
11460
|
+
});
|
|
11461
|
+
result.push({ category: lvl0Key || null, hits: markedHits });
|
|
11462
|
+
}
|
|
11463
|
+
return result;
|
|
11464
|
+
}
|
|
11465
|
+
function getGlobalIndexMulti(groups, groupIndex, hitIndex) {
|
|
11466
|
+
let index = 0;
|
|
11467
|
+
for (let i = 0; i < groupIndex; i++)
|
|
11468
|
+
index += groups[i].hits.length;
|
|
11469
|
+
return index + hitIndex;
|
|
11470
|
+
}
|
|
11471
|
+
function getHitKey(hit, index) {
|
|
11472
|
+
if ('objectID' in hit)
|
|
11473
|
+
return hit.objectID;
|
|
11474
|
+
return `suggestion-${hit.url}-${index}`;
|
|
11475
|
+
}
|
|
11476
|
+
function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, query, isLoading, error, translations = {}, sources: _sources = [], }) {
|
|
11477
|
+
const listRef = useRef(null);
|
|
11478
|
+
useEffect(() => {
|
|
11479
|
+
if (listRef.current && hits.length > 0) {
|
|
11480
|
+
const selectedItem = listRef.current.children[selectedIndex];
|
|
11481
|
+
if (selectedItem)
|
|
11482
|
+
selectedItem.scrollIntoView({ block: 'nearest' });
|
|
11483
|
+
}
|
|
11484
|
+
}, [selectedIndex, hits.length]);
|
|
11485
|
+
if (!query) {
|
|
11486
|
+
return (React.createElement("div", { className: "seekora-docsearch-empty" },
|
|
11487
|
+
React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
|
|
11488
|
+
}
|
|
11489
|
+
if (isLoading && hits.length === 0) {
|
|
11490
|
+
return (React.createElement("div", { className: "seekora-docsearch-loading" },
|
|
11491
|
+
React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
|
|
11492
|
+
React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
11493
|
+
React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "50", strokeDashoffset: "15" },
|
|
11494
|
+
React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
|
|
11495
|
+
React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
|
|
11496
|
+
}
|
|
11497
|
+
if (error) {
|
|
11498
|
+
return (React.createElement("div", { className: "seekora-docsearch-error" },
|
|
11499
|
+
React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
|
|
11500
|
+
}
|
|
11501
|
+
if (hits.length === 0 && query) {
|
|
11502
|
+
return (React.createElement("div", { className: "seekora-docsearch-no-results" },
|
|
11503
|
+
React.createElement("p", { className: "seekora-docsearch-no-results-text" }, translations.noResultsText || `No results found for "${query}"`)));
|
|
11504
|
+
}
|
|
11505
|
+
const displayGroups = groupedHits && groupedHits.length > 0
|
|
11506
|
+
? groupedHits.map(g => ({ category: g.source.name, sourceId: g.source.id, openInNewTab: g.source.openInNewTab, hits: g.items }))
|
|
11507
|
+
: groupHitsByHierarchy(hits).map(g => ({ category: g.category, sourceId: 'default', openInNewTab: false, hits: g.hits }));
|
|
11508
|
+
return (React.createElement("div", { className: "seekora-docsearch-results" },
|
|
11509
|
+
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" },
|
|
11510
|
+
group.category && React.createElement("div", { className: "seekora-docsearch-results-group-header" }, group.category),
|
|
11511
|
+
React.createElement("ul", { className: "seekora-docsearch-results-group-items" }, group.hits.map((hit, hitIndex) => {
|
|
11512
|
+
const globalIndex = getGlobalIndexMulti(displayGroups, groupIndex, hitIndex);
|
|
11513
|
+
const extHit = hit;
|
|
11514
|
+
return (React.createElement("li", { key: getHitKey(hit, hitIndex) },
|
|
11515
|
+
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 })));
|
|
11516
|
+
}))))))));
|
|
11517
|
+
}
|
|
11518
|
+
|
|
11519
|
+
function Footer({ translations = {} }) {
|
|
11520
|
+
return (React.createElement("footer", { className: "seekora-docsearch-footer" },
|
|
11521
|
+
React.createElement("div", { className: "seekora-docsearch-footer-commands" },
|
|
11522
|
+
React.createElement("ul", { className: "seekora-docsearch-footer-commands-list" },
|
|
11523
|
+
React.createElement("li", null,
|
|
11524
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11525
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u21B5"),
|
|
11526
|
+
React.createElement("span", null, "to select"))),
|
|
11527
|
+
React.createElement("li", null,
|
|
11528
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11529
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2191"),
|
|
11530
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2193"),
|
|
11531
|
+
React.createElement("span", null, "to navigate"))),
|
|
11532
|
+
React.createElement("li", null,
|
|
11533
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11534
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "esc"),
|
|
11535
|
+
React.createElement("span", null, translations.closeText || 'to close'))))),
|
|
11536
|
+
React.createElement("div", { className: "seekora-docsearch-footer-logo" },
|
|
11537
|
+
React.createElement("span", { className: "seekora-docsearch-footer-logo-text" }, translations.searchByText || 'Search by'),
|
|
11538
|
+
React.createElement("a", { href: "https://seekora.ai", target: "_blank", rel: "noopener noreferrer", className: "seekora-docsearch-footer-logo-link" },
|
|
11539
|
+
React.createElement("span", { className: "seekora-docsearch-logo", style: { fontFamily: 'system-ui', fontSize: 14, fontWeight: 600 } }, "Seekora")))));
|
|
11540
|
+
}
|
|
11541
|
+
|
|
11542
|
+
function useKeyboard(options) {
|
|
11543
|
+
const { isOpen, onOpen, onClose, onSelectNext, onSelectPrev, onEnter, disableShortcut = false, shortcutKey = 'k', } = options;
|
|
11544
|
+
const handleGlobalKeyDown = useCallback((event) => {
|
|
11545
|
+
if (!disableShortcut && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey) {
|
|
11546
|
+
event.preventDefault();
|
|
11547
|
+
if (isOpen) {
|
|
11548
|
+
onClose();
|
|
11549
|
+
}
|
|
11550
|
+
else {
|
|
11551
|
+
onOpen();
|
|
11552
|
+
}
|
|
11553
|
+
return;
|
|
11554
|
+
}
|
|
11555
|
+
if (!disableShortcut && event.key === '/' && !isOpen) {
|
|
11556
|
+
const target = event.target;
|
|
11557
|
+
const isInput = target.tagName === 'INPUT' ||
|
|
11558
|
+
target.tagName === 'TEXTAREA' ||
|
|
11559
|
+
target.isContentEditable;
|
|
11560
|
+
if (!isInput) {
|
|
11561
|
+
event.preventDefault();
|
|
11562
|
+
onOpen();
|
|
11563
|
+
}
|
|
11564
|
+
}
|
|
11565
|
+
}, [isOpen, onOpen, onClose, disableShortcut, shortcutKey]);
|
|
11566
|
+
const handleModalKeyDown = useCallback((event) => {
|
|
11567
|
+
switch (event.key) {
|
|
11568
|
+
case 'Escape':
|
|
11569
|
+
event.preventDefault();
|
|
11570
|
+
onClose();
|
|
11571
|
+
break;
|
|
11572
|
+
case 'ArrowDown':
|
|
11573
|
+
event.preventDefault();
|
|
11574
|
+
onSelectNext();
|
|
11575
|
+
break;
|
|
11576
|
+
case 'ArrowUp':
|
|
11577
|
+
event.preventDefault();
|
|
11578
|
+
onSelectPrev();
|
|
11579
|
+
break;
|
|
11580
|
+
case 'Enter':
|
|
11581
|
+
event.preventDefault();
|
|
11582
|
+
onEnter();
|
|
11583
|
+
break;
|
|
11584
|
+
case 'Tab':
|
|
11585
|
+
if (event.shiftKey) {
|
|
11586
|
+
onSelectPrev();
|
|
11587
|
+
}
|
|
11588
|
+
else {
|
|
11589
|
+
onSelectNext();
|
|
11590
|
+
}
|
|
11591
|
+
event.preventDefault();
|
|
11592
|
+
break;
|
|
11593
|
+
}
|
|
11594
|
+
}, [onClose, onSelectNext, onSelectPrev, onEnter]);
|
|
11595
|
+
useEffect(() => {
|
|
11596
|
+
document.addEventListener('keydown', handleGlobalKeyDown);
|
|
11597
|
+
return () => {
|
|
11598
|
+
document.removeEventListener('keydown', handleGlobalKeyDown);
|
|
11599
|
+
};
|
|
11600
|
+
}, [handleGlobalKeyDown]);
|
|
11601
|
+
return {
|
|
11602
|
+
handleModalKeyDown,
|
|
11603
|
+
};
|
|
11604
|
+
}
|
|
11605
|
+
function getShortcutText(key = 'K') {
|
|
11606
|
+
if (typeof navigator === 'undefined') {
|
|
11607
|
+
return `⌘${key}`;
|
|
11608
|
+
}
|
|
11609
|
+
const isMac = navigator.platform.toLowerCase().includes('mac');
|
|
11610
|
+
return isMac ? `⌘${key}` : `Ctrl+${key}`;
|
|
11611
|
+
}
|
|
11612
|
+
|
|
11613
|
+
function DocSearchButton({ onClick, placeholder = 'Search documentation...' }) {
|
|
11614
|
+
const shortcutText = getShortcutText('K');
|
|
11615
|
+
return (React.createElement("button", { type: "button", className: "seekora-docsearch-button", onClick: onClick, "aria-label": "Search documentation" },
|
|
11616
|
+
React.createElement("span", { className: "seekora-docsearch-button-icon" },
|
|
11617
|
+
React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
|
|
11618
|
+
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" }))),
|
|
11619
|
+
React.createElement("span", { className: "seekora-docsearch-button-placeholder" }, placeholder),
|
|
11620
|
+
React.createElement("span", { className: "seekora-docsearch-button-keys" },
|
|
11621
|
+
React.createElement("kbd", { className: "seekora-docsearch-button-key" }, shortcutText))));
|
|
11622
|
+
}
|
|
11623
|
+
|
|
11624
|
+
const initialState = {
|
|
11625
|
+
query: '',
|
|
11626
|
+
results: [],
|
|
11627
|
+
suggestions: [],
|
|
11628
|
+
groupedSuggestions: [],
|
|
11629
|
+
isLoading: false,
|
|
11630
|
+
error: null,
|
|
11631
|
+
selectedIndex: 0,
|
|
11632
|
+
mode: 'suggestions',
|
|
11633
|
+
};
|
|
11634
|
+
function reducer(state, action) {
|
|
11635
|
+
switch (action.type) {
|
|
11636
|
+
case 'SET_QUERY':
|
|
11637
|
+
return { ...state, query: action.payload, selectedIndex: 0 };
|
|
11638
|
+
case 'SET_RESULTS':
|
|
11639
|
+
return { ...state, results: action.payload, mode: 'results' };
|
|
11640
|
+
case 'SET_SUGGESTIONS':
|
|
11641
|
+
return { ...state, suggestions: action.payload, mode: 'suggestions' };
|
|
11642
|
+
case 'SET_GROUPED_SUGGESTIONS': {
|
|
11643
|
+
const flatSuggestions = action.payload.flatMap(group => group.items.map(item => ({ ...item, _source: group.source.id })));
|
|
11644
|
+
return { ...state, groupedSuggestions: action.payload, suggestions: flatSuggestions, mode: 'suggestions' };
|
|
11645
|
+
}
|
|
11646
|
+
case 'SET_LOADING':
|
|
11647
|
+
return { ...state, isLoading: action.payload };
|
|
11648
|
+
case 'SET_ERROR':
|
|
11649
|
+
return { ...state, error: action.payload };
|
|
11650
|
+
case 'SET_SELECTED_INDEX':
|
|
11651
|
+
return { ...state, selectedIndex: action.payload };
|
|
11652
|
+
case 'SELECT_NEXT': {
|
|
11653
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11654
|
+
const maxIndex = items.length - 1;
|
|
11655
|
+
return { ...state, selectedIndex: state.selectedIndex >= maxIndex ? 0 : state.selectedIndex + 1 };
|
|
11656
|
+
}
|
|
11657
|
+
case 'SELECT_PREV': {
|
|
11658
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11659
|
+
const maxIndex = items.length - 1;
|
|
11660
|
+
return { ...state, selectedIndex: state.selectedIndex <= 0 ? maxIndex : state.selectedIndex - 1 };
|
|
11661
|
+
}
|
|
11662
|
+
case 'SET_MODE':
|
|
11663
|
+
return { ...state, mode: action.payload, selectedIndex: 0 };
|
|
11664
|
+
case 'RESET':
|
|
11665
|
+
return initialState;
|
|
11666
|
+
default:
|
|
11667
|
+
return state;
|
|
11668
|
+
}
|
|
11669
|
+
}
|
|
11670
|
+
function useDocSearch(options) {
|
|
11671
|
+
const { apiEndpoint, apiKey, sources, maxResults = 10, debounceMs = 200 } = options;
|
|
11672
|
+
const searchSources = sources || (apiEndpoint ? [{
|
|
11673
|
+
id: 'default',
|
|
11674
|
+
name: 'Results',
|
|
11675
|
+
endpoint: apiEndpoint,
|
|
11676
|
+
apiKey,
|
|
11677
|
+
maxResults,
|
|
11678
|
+
}] : []);
|
|
11679
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
11680
|
+
const abortControllersRef = useRef(new Map());
|
|
11681
|
+
const debounceTimerRef = useRef(null);
|
|
11682
|
+
const defaultTransform = (data, sourceId) => {
|
|
11683
|
+
const items = data.data?.suggestions || data.data?.results || data.suggestions || data.results || data.hits || [];
|
|
11684
|
+
return items.map((item) => ({
|
|
11685
|
+
url: item.url || item.route || '',
|
|
11686
|
+
title: item.title?.replace?.(/<\/?mark>/g, '') || item.title || '',
|
|
11687
|
+
content: item.content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || item.description || '',
|
|
11688
|
+
description: item.description || item.content?.substring?.(0, 100) || '',
|
|
11689
|
+
category: item.category || item.hierarchy?.lvl0 || '',
|
|
11690
|
+
hierarchy: item.hierarchy,
|
|
11691
|
+
route: item.route,
|
|
11692
|
+
parentTitle: item.parent_title || item.parentTitle,
|
|
11693
|
+
_source: sourceId,
|
|
11694
|
+
}));
|
|
11695
|
+
};
|
|
11696
|
+
const transformPublicSearchResults = useCallback((data, sourceId) => {
|
|
11697
|
+
const results = data?.data?.results || [];
|
|
11698
|
+
return results.map((item) => {
|
|
11699
|
+
const doc = item.document || item;
|
|
11700
|
+
return {
|
|
11701
|
+
url: doc.url || doc.route || '',
|
|
11702
|
+
title: (doc.title || doc.name || '').replace?.(/<\/?mark>/g, '') || '',
|
|
11703
|
+
content: (doc.content || doc.description || '').replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || '',
|
|
11704
|
+
description: doc.description || doc.content?.substring?.(0, 100) || '',
|
|
11705
|
+
category: doc.category || doc.hierarchy?.lvl0 || '',
|
|
11706
|
+
hierarchy: doc.hierarchy,
|
|
11707
|
+
route: doc.route,
|
|
11708
|
+
parentTitle: doc.parent_title || doc.parentTitle,
|
|
11709
|
+
_source: sourceId,
|
|
11710
|
+
};
|
|
11711
|
+
});
|
|
11712
|
+
}, []);
|
|
11713
|
+
const fetchFromSource = useCallback(async (source, query, signal) => {
|
|
11714
|
+
const minLength = source.minQueryLength ?? 1;
|
|
11715
|
+
if (query.length < minLength)
|
|
11716
|
+
return [];
|
|
11717
|
+
try {
|
|
11718
|
+
if (source.storeId) {
|
|
11719
|
+
const baseUrl = source.endpoint.replace(/\/$/, '');
|
|
11720
|
+
const searchUrl = `${baseUrl}/api/v1/search`;
|
|
11721
|
+
const headers = {
|
|
11722
|
+
'Content-Type': 'application/json',
|
|
11723
|
+
'x-storeid': source.storeId,
|
|
11724
|
+
...(source.storeSecret && { 'x-storesecret': source.storeSecret }),
|
|
11725
|
+
};
|
|
11726
|
+
const response = await fetch(searchUrl, {
|
|
11727
|
+
method: 'POST',
|
|
11728
|
+
headers,
|
|
11729
|
+
body: JSON.stringify({ q: query.trim() || '*', per_page: source.maxResults || 8 }),
|
|
11730
|
+
signal,
|
|
11731
|
+
});
|
|
11732
|
+
if (!response.ok)
|
|
11733
|
+
return [];
|
|
11734
|
+
const data = await response.json();
|
|
11735
|
+
if (source.transformResults) {
|
|
11736
|
+
return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
|
|
11737
|
+
}
|
|
11738
|
+
return transformPublicSearchResults(data, source.id);
|
|
11739
|
+
}
|
|
11740
|
+
const url = new URL(source.endpoint);
|
|
11741
|
+
url.searchParams.set('query', query);
|
|
11742
|
+
url.searchParams.set('limit', String(source.maxResults || 8));
|
|
11743
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
11744
|
+
if (source.apiKey)
|
|
11745
|
+
headers['X-Docs-API-Key'] = source.apiKey;
|
|
11746
|
+
const response = await fetch(url.toString(), { method: 'GET', headers, signal });
|
|
11747
|
+
if (!response.ok)
|
|
11748
|
+
return [];
|
|
11749
|
+
const data = await response.json();
|
|
11750
|
+
if (source.transformResults) {
|
|
11751
|
+
return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
|
|
11752
|
+
}
|
|
11753
|
+
return defaultTransform(data, source.id);
|
|
11754
|
+
}
|
|
11755
|
+
catch (error) {
|
|
11756
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
11757
|
+
throw error;
|
|
11758
|
+
return [];
|
|
11759
|
+
}
|
|
11760
|
+
}, [transformPublicSearchResults]);
|
|
11761
|
+
const fetchSuggestions = useCallback(async (query) => {
|
|
11762
|
+
if (!query.trim()) {
|
|
11763
|
+
dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: [] });
|
|
11764
|
+
return;
|
|
11765
|
+
}
|
|
11766
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11767
|
+
abortControllersRef.current.clear();
|
|
11768
|
+
dispatch({ type: 'SET_LOADING', payload: true });
|
|
11769
|
+
dispatch({ type: 'SET_ERROR', payload: null });
|
|
11770
|
+
try {
|
|
11771
|
+
const results = await Promise.all(searchSources.map(async (source) => {
|
|
11772
|
+
const controller = new AbortController();
|
|
11773
|
+
abortControllersRef.current.set(source.id, controller);
|
|
11774
|
+
const items = await fetchFromSource(source, query, controller.signal);
|
|
11775
|
+
return { source, items };
|
|
11776
|
+
}));
|
|
11777
|
+
const groupedResults = results.filter(r => r.items.length > 0);
|
|
11778
|
+
dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: groupedResults });
|
|
11779
|
+
}
|
|
11780
|
+
catch (error) {
|
|
11781
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
11782
|
+
return;
|
|
11783
|
+
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Search failed' });
|
|
11784
|
+
}
|
|
11785
|
+
finally {
|
|
11786
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
11787
|
+
}
|
|
11788
|
+
}, [searchSources, fetchFromSource]);
|
|
11789
|
+
const search = useCallback((q) => fetchSuggestions(q), [fetchSuggestions]);
|
|
11790
|
+
const setQuery = useCallback((query) => {
|
|
11791
|
+
dispatch({ type: 'SET_QUERY', payload: query });
|
|
11792
|
+
if (debounceTimerRef.current)
|
|
11793
|
+
clearTimeout(debounceTimerRef.current);
|
|
11794
|
+
debounceTimerRef.current = setTimeout(() => fetchSuggestions(query), debounceMs);
|
|
11795
|
+
}, [fetchSuggestions, debounceMs]);
|
|
11796
|
+
const selectNext = useCallback(() => dispatch({ type: 'SELECT_NEXT' }), []);
|
|
11797
|
+
const selectPrev = useCallback(() => dispatch({ type: 'SELECT_PREV' }), []);
|
|
11798
|
+
const setSelectedIndex = useCallback((index) => dispatch({ type: 'SET_SELECTED_INDEX', payload: index }), []);
|
|
11799
|
+
const reset = useCallback(() => {
|
|
11800
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11801
|
+
abortControllersRef.current.clear();
|
|
11802
|
+
if (debounceTimerRef.current)
|
|
11803
|
+
clearTimeout(debounceTimerRef.current);
|
|
11804
|
+
dispatch({ type: 'RESET' });
|
|
11805
|
+
}, []);
|
|
11806
|
+
const getSelectedItem = useCallback(() => {
|
|
11807
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11808
|
+
return items[state.selectedIndex] || null;
|
|
11809
|
+
}, [state.mode, state.results, state.suggestions, state.selectedIndex]);
|
|
11810
|
+
useEffect(() => {
|
|
11811
|
+
return () => {
|
|
11812
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11813
|
+
abortControllersRef.current.clear();
|
|
11814
|
+
if (debounceTimerRef.current)
|
|
11815
|
+
clearTimeout(debounceTimerRef.current);
|
|
11816
|
+
};
|
|
11817
|
+
}, []);
|
|
11818
|
+
return {
|
|
11819
|
+
...state,
|
|
11820
|
+
sources: searchSources,
|
|
11821
|
+
setQuery,
|
|
11822
|
+
search,
|
|
11823
|
+
fetchSuggestions,
|
|
11824
|
+
selectNext,
|
|
11825
|
+
selectPrev,
|
|
11826
|
+
setSelectedIndex,
|
|
11827
|
+
reset,
|
|
11828
|
+
getSelectedItem,
|
|
11829
|
+
};
|
|
11830
|
+
}
|
|
11831
|
+
|
|
11832
|
+
function transformResults(results) {
|
|
11833
|
+
return results.map((result) => {
|
|
11834
|
+
const url = result.url || result.route || result.link || '';
|
|
11835
|
+
const title = result.title || result.name || '';
|
|
11836
|
+
const content = result.content || result.description || result.snippet || '';
|
|
11837
|
+
const description = result.description || result.content?.substring?.(0, 150) || '';
|
|
11838
|
+
const hierarchy = {};
|
|
11839
|
+
if (result.hierarchy) {
|
|
11840
|
+
hierarchy.lvl0 = result.hierarchy.lvl0;
|
|
11841
|
+
hierarchy.lvl1 = result.hierarchy.lvl1;
|
|
11842
|
+
hierarchy.lvl2 = result.hierarchy.lvl2;
|
|
11843
|
+
}
|
|
11844
|
+
else {
|
|
11845
|
+
if (result.category)
|
|
11846
|
+
hierarchy.lvl0 = result.category;
|
|
11847
|
+
if (result.parent_title || result.parentTitle)
|
|
11848
|
+
hierarchy.lvl1 = result.parent_title || result.parentTitle;
|
|
11849
|
+
}
|
|
11850
|
+
return {
|
|
11851
|
+
url,
|
|
11852
|
+
title: title?.replace?.(/<\/?mark>/g, '') || title,
|
|
11853
|
+
content: content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 200) || content,
|
|
11854
|
+
description: description?.replace?.(/<\/?mark>/g, '') || description,
|
|
11855
|
+
category: result.category || hierarchy.lvl0 || '',
|
|
11856
|
+
hierarchy,
|
|
11857
|
+
route: result.route,
|
|
11858
|
+
parentTitle: result.parent_title || result.parentTitle,
|
|
11859
|
+
type: result.type || '',
|
|
11860
|
+
anchor: result.anchor || '',
|
|
11861
|
+
_source: 'seekora',
|
|
11862
|
+
};
|
|
11863
|
+
});
|
|
11864
|
+
}
|
|
11865
|
+
function useSeekoraSearch$1(options) {
|
|
11866
|
+
const { storeId, storeSecret, apiEndpoint, maxResults = 20, debounceMs = 200, analyticsTags = ['docsearch'], groupField, groupSize, } = options;
|
|
11867
|
+
const [query, setQueryState] = useState('');
|
|
11868
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
11869
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
11870
|
+
const [error, setError] = useState(null);
|
|
11871
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
11872
|
+
const clientRef = useRef(null);
|
|
11873
|
+
const lastSearchContextRef = useRef(null);
|
|
11874
|
+
const debounceTimerRef = useRef(null);
|
|
11875
|
+
const abortControllerRef = useRef(null);
|
|
11876
|
+
useEffect(() => {
|
|
11877
|
+
if (!storeId)
|
|
11878
|
+
return;
|
|
11879
|
+
const config = {
|
|
11880
|
+
storeId,
|
|
11881
|
+
readSecret: storeSecret,
|
|
11882
|
+
logLevel: 'warn',
|
|
11883
|
+
enableContextCollection: true,
|
|
11884
|
+
};
|
|
11885
|
+
if (apiEndpoint) {
|
|
11886
|
+
if (['local', 'stage', 'production'].includes(apiEndpoint)) {
|
|
11887
|
+
config.environment = apiEndpoint;
|
|
11888
|
+
}
|
|
11889
|
+
else {
|
|
11890
|
+
config.baseUrl = apiEndpoint;
|
|
11891
|
+
}
|
|
11892
|
+
}
|
|
11893
|
+
try {
|
|
11894
|
+
clientRef.current = new SeekoraClient(config);
|
|
11895
|
+
}
|
|
11896
|
+
catch (err) {
|
|
11897
|
+
console.error('Failed to initialize SeekoraClient:', err);
|
|
11898
|
+
setError('Failed to initialize search client');
|
|
11899
|
+
}
|
|
11900
|
+
return () => {
|
|
11901
|
+
clientRef.current = null;
|
|
11902
|
+
};
|
|
11903
|
+
}, [storeId, storeSecret, apiEndpoint]);
|
|
11904
|
+
const performSearch = useCallback(async (searchQuery) => {
|
|
11905
|
+
if (!clientRef.current) {
|
|
11906
|
+
setError('Search client not initialized');
|
|
11907
|
+
return;
|
|
11908
|
+
}
|
|
11909
|
+
if (!searchQuery.trim()) {
|
|
11910
|
+
setSuggestions([]);
|
|
11911
|
+
return;
|
|
11912
|
+
}
|
|
11913
|
+
if (abortControllerRef.current)
|
|
11914
|
+
abortControllerRef.current.abort();
|
|
11915
|
+
abortControllerRef.current = new AbortController();
|
|
11916
|
+
setIsLoading(true);
|
|
11917
|
+
setError(null);
|
|
11918
|
+
try {
|
|
11919
|
+
const response = await clientRef.current.search(searchQuery, {
|
|
11920
|
+
per_page: maxResults,
|
|
11921
|
+
analytics_tags: analyticsTags,
|
|
11922
|
+
return_fields: [
|
|
11923
|
+
'hierarchy.lvl0', 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
|
|
11924
|
+
'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6',
|
|
11925
|
+
'content', 'type', 'url', 'title', 'anchor'
|
|
11926
|
+
],
|
|
11927
|
+
snippet_fields: [
|
|
11928
|
+
'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
|
|
11929
|
+
'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6', 'content'
|
|
11930
|
+
],
|
|
11931
|
+
snippet_prefix: '<mark>',
|
|
11932
|
+
snippet_suffix: '</mark>',
|
|
11933
|
+
include_snippets: true,
|
|
11934
|
+
group_field: groupField,
|
|
11935
|
+
group_size: groupSize,
|
|
11936
|
+
});
|
|
11937
|
+
if (abortControllerRef.current?.signal.aborted)
|
|
11938
|
+
return;
|
|
11939
|
+
if (response?.context)
|
|
11940
|
+
lastSearchContextRef.current = response.context;
|
|
11941
|
+
setSuggestions(transformResults(response.results || []));
|
|
11942
|
+
setSelectedIndex(0);
|
|
11943
|
+
}
|
|
11944
|
+
catch (err) {
|
|
11945
|
+
if (err instanceof Error && err.name === 'AbortError')
|
|
11946
|
+
return;
|
|
11947
|
+
console.error('Search failed:', err);
|
|
11948
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
11949
|
+
setSuggestions([]);
|
|
11950
|
+
}
|
|
11951
|
+
finally {
|
|
11952
|
+
setIsLoading(false);
|
|
11953
|
+
}
|
|
11954
|
+
}, [maxResults, analyticsTags, groupField, groupSize]);
|
|
11955
|
+
const setQuery = useCallback((newQuery) => {
|
|
11956
|
+
setQueryState(newQuery);
|
|
11957
|
+
setSelectedIndex(0);
|
|
11958
|
+
if (debounceTimerRef.current)
|
|
11959
|
+
clearTimeout(debounceTimerRef.current);
|
|
11960
|
+
debounceTimerRef.current = setTimeout(() => performSearch(newQuery), debounceMs);
|
|
11961
|
+
}, [performSearch, debounceMs]);
|
|
11962
|
+
const selectNext = useCallback(() => {
|
|
11963
|
+
setSelectedIndex((prev) => (prev >= suggestions.length - 1 ? 0 : prev + 1));
|
|
11964
|
+
}, [suggestions.length]);
|
|
11965
|
+
const selectPrev = useCallback(() => {
|
|
11966
|
+
setSelectedIndex((prev) => (prev <= 0 ? suggestions.length - 1 : prev - 1));
|
|
11967
|
+
}, [suggestions.length]);
|
|
11968
|
+
const reset = useCallback(() => {
|
|
11969
|
+
if (abortControllerRef.current)
|
|
11970
|
+
abortControllerRef.current.abort();
|
|
11971
|
+
if (debounceTimerRef.current)
|
|
11972
|
+
clearTimeout(debounceTimerRef.current);
|
|
11973
|
+
setQueryState('');
|
|
11974
|
+
setSuggestions([]);
|
|
11975
|
+
setIsLoading(false);
|
|
11976
|
+
setError(null);
|
|
11977
|
+
setSelectedIndex(0);
|
|
11978
|
+
}, []);
|
|
11979
|
+
const getSelectedItem = useCallback(() => {
|
|
11980
|
+
return suggestions[selectedIndex] || null;
|
|
11981
|
+
}, [suggestions, selectedIndex]);
|
|
11982
|
+
const trackDocClick = useCallback((hit, position) => {
|
|
11983
|
+
const client = clientRef.current;
|
|
11984
|
+
if (!client?.trackEvent)
|
|
11985
|
+
return;
|
|
11986
|
+
const context = lastSearchContextRef.current ?? undefined;
|
|
11987
|
+
const itemId = hit.url || hit.id || hit.title || String(position);
|
|
11988
|
+
client.trackEvent({
|
|
11989
|
+
event_name: 'doc_click',
|
|
11990
|
+
clicked_item_id: itemId,
|
|
11991
|
+
metadata: { position, result: hit, source: 'docsearch' },
|
|
11992
|
+
}, context);
|
|
11993
|
+
}, []);
|
|
11994
|
+
useEffect(() => {
|
|
11995
|
+
return () => {
|
|
11996
|
+
if (abortControllerRef.current)
|
|
11997
|
+
abortControllerRef.current.abort();
|
|
11998
|
+
if (debounceTimerRef.current)
|
|
11999
|
+
clearTimeout(debounceTimerRef.current);
|
|
12000
|
+
};
|
|
12001
|
+
}, []);
|
|
12002
|
+
return {
|
|
12003
|
+
query,
|
|
12004
|
+
suggestions,
|
|
12005
|
+
isLoading,
|
|
12006
|
+
error,
|
|
12007
|
+
selectedIndex,
|
|
12008
|
+
setQuery,
|
|
12009
|
+
selectNext,
|
|
12010
|
+
selectPrev,
|
|
12011
|
+
setSelectedIndex,
|
|
12012
|
+
reset,
|
|
12013
|
+
getSelectedItem,
|
|
12014
|
+
trackDocClick,
|
|
12015
|
+
};
|
|
12016
|
+
}
|
|
12017
|
+
|
|
12018
|
+
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', }) {
|
|
12019
|
+
const [isOpen, setIsOpen] = useState(initialOpen);
|
|
12020
|
+
const useSeekoraSDK = !!storeId;
|
|
12021
|
+
const seekoraSearch = useSeekoraSearch$1({
|
|
12022
|
+
storeId: storeId || '',
|
|
12023
|
+
storeSecret,
|
|
12024
|
+
apiEndpoint: seekoraApiEndpoint,
|
|
12025
|
+
maxResults,
|
|
12026
|
+
debounceMs,
|
|
12027
|
+
analyticsTags: ['docsearch'],
|
|
12028
|
+
});
|
|
12029
|
+
const legacySearch = useDocSearch({
|
|
12030
|
+
apiEndpoint,
|
|
12031
|
+
apiKey,
|
|
12032
|
+
sources,
|
|
12033
|
+
maxResults,
|
|
12034
|
+
debounceMs,
|
|
12035
|
+
});
|
|
12036
|
+
const { query, suggestions, isLoading, error, selectedIndex, setQuery, selectNext, selectPrev, setSelectedIndex, reset, getSelectedItem, } = useSeekoraSDK ? seekoraSearch : legacySearch;
|
|
12037
|
+
const groupedSuggestions = useSeekoraSDK ? undefined : legacySearch.groupedSuggestions;
|
|
12038
|
+
const results = useSeekoraSDK ? suggestions : legacySearch.results;
|
|
12039
|
+
const mode = useSeekoraSDK ? 'suggestions' : legacySearch.mode;
|
|
12040
|
+
const searchSources = useSeekoraSDK
|
|
12041
|
+
? [{ id: 'seekora', name: 'Results', endpoint: '' }]
|
|
12042
|
+
: legacySearch.sources;
|
|
12043
|
+
const handleOpen = useCallback(() => setIsOpen(true), []);
|
|
12044
|
+
const handleClose = useCallback(() => {
|
|
12045
|
+
setIsOpen(false);
|
|
12046
|
+
reset();
|
|
12047
|
+
onClose?.();
|
|
12048
|
+
}, [reset, onClose]);
|
|
12049
|
+
const handleSelect = useCallback((hit) => {
|
|
12050
|
+
if (useSeekoraSDK && seekoraSearch.trackDocClick) {
|
|
12051
|
+
seekoraSearch.trackDocClick(hit, selectedIndex + 1);
|
|
12052
|
+
}
|
|
12053
|
+
if (onSelect) {
|
|
12054
|
+
onSelect(hit);
|
|
12055
|
+
}
|
|
12056
|
+
else {
|
|
12057
|
+
window.location.href = hit.url;
|
|
12058
|
+
}
|
|
12059
|
+
handleClose();
|
|
12060
|
+
}, [onSelect, handleClose, useSeekoraSDK, seekoraSearch, selectedIndex]);
|
|
12061
|
+
const handleEnter = useCallback(() => {
|
|
12062
|
+
const selectedItem = getSelectedItem();
|
|
12063
|
+
if (selectedItem)
|
|
12064
|
+
handleSelect(selectedItem);
|
|
12065
|
+
}, [getSelectedItem, handleSelect]);
|
|
12066
|
+
const { handleModalKeyDown } = useKeyboard({
|
|
12067
|
+
isOpen,
|
|
12068
|
+
onOpen: handleOpen,
|
|
12069
|
+
onClose: handleClose,
|
|
12070
|
+
onSelectNext: selectNext,
|
|
12071
|
+
onSelectPrev: selectPrev,
|
|
12072
|
+
onEnter: handleEnter,
|
|
12073
|
+
disableShortcut,
|
|
12074
|
+
shortcutKey,
|
|
12075
|
+
});
|
|
12076
|
+
const handleKeyDown = useCallback((event) => handleModalKeyDown(event), [handleModalKeyDown]);
|
|
12077
|
+
const displayHits = mode === 'results' ? results : suggestions;
|
|
12078
|
+
return (React.createElement(React.Fragment, null,
|
|
12079
|
+
renderButton && (React.createElement(ButtonComponent, { onClick: handleOpen, placeholder: translations.buttonText || placeholder })),
|
|
12080
|
+
React.createElement(Modal, { isOpen: isOpen, onClose: handleClose },
|
|
12081
|
+
React.createElement("div", { className: "seekora-docsearch-modal", onKeyDown: handleKeyDown },
|
|
12082
|
+
React.createElement("header", { className: "seekora-docsearch-header" },
|
|
12083
|
+
React.createElement(SearchBox, { value: query, onChange: setQuery, onKeyDown: handleKeyDown, placeholder: placeholder, isLoading: isLoading, onClear: reset }),
|
|
12084
|
+
React.createElement("button", { type: "button", className: "seekora-docsearch-close", onClick: handleClose, "aria-label": "Close search" },
|
|
12085
|
+
React.createElement("span", { className: "seekora-docsearch-close-text" }, "esc"))),
|
|
12086
|
+
React.createElement("div", { className: "seekora-docsearch-body" },
|
|
12087
|
+
React.createElement(Results, { hits: displayHits, groupedHits: groupedSuggestions, selectedIndex: selectedIndex, onSelect: handleSelect, onHover: setSelectedIndex, query: query, isLoading: isLoading, error: error, translations: translations, sources: searchSources })),
|
|
12088
|
+
React.createElement(Footer, { translations: translations })))));
|
|
12089
|
+
}
|
|
12090
|
+
|
|
10564
12091
|
/**
|
|
10565
12092
|
* useSeekoraSearch Hook
|
|
10566
12093
|
*
|
|
@@ -10626,7 +12153,9 @@ const useSeekoraSearch = ({ client, autoTrack = true, }) => {
|
|
|
10626
12153
|
/**
|
|
10627
12154
|
* useAnalytics Hook
|
|
10628
12155
|
*
|
|
10629
|
-
* Hook for tracking analytics events with the Seekora SDK
|
|
12156
|
+
* Hook for tracking analytics events with the Seekora SDK.
|
|
12157
|
+
* Supports Analytics V3 payload fields (event_ts, anonymous_id, orgcode, xstoreid);
|
|
12158
|
+
* the SDK sends both legacy and v3 fields for backend compatibility.
|
|
10630
12159
|
*/
|
|
10631
12160
|
const useAnalytics = ({ client, enabled = true, }) => {
|
|
10632
12161
|
const trackEvent = useCallback(async (eventType, payload, context) => {
|
|
@@ -10646,14 +12175,17 @@ const useAnalytics = ({ client, enabled = true, }) => {
|
|
|
10646
12175
|
const trackClick = useCallback(async (resultId, result, context, position) => {
|
|
10647
12176
|
if (!enabled)
|
|
10648
12177
|
return;
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
12178
|
+
const pos = position ?? 0;
|
|
12179
|
+
if (client.trackClick) {
|
|
12180
|
+
await client.trackClick(resultId, pos, context);
|
|
12181
|
+
}
|
|
12182
|
+
else {
|
|
12183
|
+
await trackEvent('product_click', {
|
|
12184
|
+
clicked_item_id: resultId,
|
|
12185
|
+
metadata: { result, ...(position !== undefined && { position: pos }) },
|
|
12186
|
+
}, context);
|
|
12187
|
+
}
|
|
12188
|
+
}, [client, trackEvent, enabled]);
|
|
10657
12189
|
const trackConversion = useCallback(async (resultId, result, value, currency, context) => {
|
|
10658
12190
|
if (!enabled)
|
|
10659
12191
|
return;
|
|
@@ -11675,5 +13207,5 @@ function updateSuggestionsStyles(theme) {
|
|
|
11675
13207
|
injectSuggestionsStyles(theme, true);
|
|
11676
13208
|
}
|
|
11677
13209
|
|
|
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 };
|
|
13210
|
+
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
13211
|
//# sourceMappingURL=index.esm.js.map
|