@seekora-ai/ui-sdk-react 0.2.0 → 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.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var React = require('react');
|
|
4
|
+
var reactDom = require('react-dom');
|
|
5
|
+
var searchSdk = require('@seekora-ai/search-sdk');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Field Mapping Utilities
|
|
@@ -567,6 +569,688 @@ class SearchStateManager {
|
|
|
567
569
|
}
|
|
568
570
|
}
|
|
569
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Seekora Device Fingerprinting Module
|
|
574
|
+
*
|
|
575
|
+
* A lightweight, dependency-free device fingerprinting solution that collects
|
|
576
|
+
* various browser and device signals to generate a unique visitor identifier.
|
|
577
|
+
*/
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// MurmurHash3 Implementation (32-bit)
|
|
580
|
+
// ============================================================================
|
|
581
|
+
function murmurHash3(str, seed = 0) {
|
|
582
|
+
const remainder = str.length & 3; // str.length % 4
|
|
583
|
+
const bytes = str.length - remainder;
|
|
584
|
+
let h1 = seed;
|
|
585
|
+
const c1 = 0xcc9e2d51;
|
|
586
|
+
const c2 = 0x1b873593;
|
|
587
|
+
let i = 0;
|
|
588
|
+
let k1;
|
|
589
|
+
while (i < bytes) {
|
|
590
|
+
k1 =
|
|
591
|
+
(str.charCodeAt(i) & 0xff) |
|
|
592
|
+
((str.charCodeAt(++i) & 0xff) << 8) |
|
|
593
|
+
((str.charCodeAt(++i) & 0xff) << 16) |
|
|
594
|
+
((str.charCodeAt(++i) & 0xff) << 24);
|
|
595
|
+
++i;
|
|
596
|
+
k1 = Math.imul(k1, c1);
|
|
597
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
598
|
+
k1 = Math.imul(k1, c2);
|
|
599
|
+
h1 ^= k1;
|
|
600
|
+
h1 = (h1 << 13) | (h1 >>> 19);
|
|
601
|
+
h1 = Math.imul(h1, 5) + 0xe6546b64;
|
|
602
|
+
}
|
|
603
|
+
k1 = 0;
|
|
604
|
+
switch (remainder) {
|
|
605
|
+
case 3:
|
|
606
|
+
k1 ^= (str.charCodeAt(i + 2) & 0xff) << 16;
|
|
607
|
+
// fallthrough
|
|
608
|
+
case 2:
|
|
609
|
+
k1 ^= (str.charCodeAt(i + 1) & 0xff) << 8;
|
|
610
|
+
// fallthrough
|
|
611
|
+
case 1:
|
|
612
|
+
k1 ^= str.charCodeAt(i) & 0xff;
|
|
613
|
+
k1 = Math.imul(k1, c1);
|
|
614
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
615
|
+
k1 = Math.imul(k1, c2);
|
|
616
|
+
h1 ^= k1;
|
|
617
|
+
}
|
|
618
|
+
h1 ^= str.length;
|
|
619
|
+
h1 ^= h1 >>> 16;
|
|
620
|
+
h1 = Math.imul(h1, 0x85ebca6b);
|
|
621
|
+
h1 ^= h1 >>> 13;
|
|
622
|
+
h1 = Math.imul(h1, 0xc2b2ae35);
|
|
623
|
+
h1 ^= h1 >>> 16;
|
|
624
|
+
return h1 >>> 0;
|
|
625
|
+
}
|
|
626
|
+
function murmurHash3Hex(str, seed = 0) {
|
|
627
|
+
return murmurHash3(str, seed).toString(16).padStart(8, '0');
|
|
628
|
+
}
|
|
629
|
+
// ============================================================================
|
|
630
|
+
// Default Configuration
|
|
631
|
+
// ============================================================================
|
|
632
|
+
const DEFAULT_CONFIG = {
|
|
633
|
+
enableCanvas: true,
|
|
634
|
+
enableWebGL: true,
|
|
635
|
+
enableAudio: true,
|
|
636
|
+
enableFonts: true,
|
|
637
|
+
enableHardware: true,
|
|
638
|
+
timeout: 5000,
|
|
639
|
+
};
|
|
640
|
+
// ============================================================================
|
|
641
|
+
// Font List for Detection
|
|
642
|
+
// ============================================================================
|
|
643
|
+
const FONTS_TO_CHECK = [
|
|
644
|
+
// Windows fonts
|
|
645
|
+
'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS',
|
|
646
|
+
'Consolas', 'Courier New', 'Georgia', 'Impact', 'Lucida Console',
|
|
647
|
+
'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Palatino Linotype',
|
|
648
|
+
'Segoe UI', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana',
|
|
649
|
+
// macOS fonts
|
|
650
|
+
'American Typewriter', 'Andale Mono', 'Apple Chancery', 'Apple Color Emoji',
|
|
651
|
+
'Apple SD Gothic Neo', 'Arial Hebrew', 'Avenir', 'Baskerville',
|
|
652
|
+
'Big Caslon', 'Brush Script MT', 'Chalkboard', 'Cochin', 'Copperplate',
|
|
653
|
+
'Didot', 'Futura', 'Geneva', 'Gill Sans', 'Helvetica', 'Helvetica Neue',
|
|
654
|
+
'Herculanum', 'Hoefler Text', 'Lucida Grande', 'Marker Felt', 'Menlo',
|
|
655
|
+
'Monaco', 'Noteworthy', 'Optima', 'Papyrus', 'Phosphate', 'Rockwell',
|
|
656
|
+
'San Francisco', 'Savoye LET', 'SignPainter', 'Skia', 'Snell Roundhand',
|
|
657
|
+
'Zapfino',
|
|
658
|
+
// Linux fonts
|
|
659
|
+
'Cantarell', 'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif',
|
|
660
|
+
'Droid Sans', 'Droid Sans Mono', 'Droid Serif', 'FreeMono', 'FreeSans',
|
|
661
|
+
'FreeSerif', 'Liberation Mono', 'Liberation Sans', 'Liberation Serif',
|
|
662
|
+
'Noto Sans', 'Noto Serif', 'Open Sans', 'Roboto', 'Ubuntu', 'Ubuntu Mono',
|
|
663
|
+
// Common web fonts
|
|
664
|
+
'Lato', 'Montserrat', 'Oswald', 'Raleway', 'Source Sans Pro',
|
|
665
|
+
'PT Sans', 'Merriweather', 'Nunito', 'Playfair Display', 'Poppins',
|
|
666
|
+
];
|
|
667
|
+
// ============================================================================
|
|
668
|
+
// Fingerprint Class
|
|
669
|
+
// ============================================================================
|
|
670
|
+
class Fingerprint {
|
|
671
|
+
constructor(config) {
|
|
672
|
+
this.cachedResult = null;
|
|
673
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Get the complete fingerprint result
|
|
677
|
+
*/
|
|
678
|
+
async get() {
|
|
679
|
+
if (this.cachedResult) {
|
|
680
|
+
return this.cachedResult;
|
|
681
|
+
}
|
|
682
|
+
const components = await this.collectComponents();
|
|
683
|
+
const confidence = this.calculateConfidence(components);
|
|
684
|
+
const visitorId = this.generateVisitorId(components);
|
|
685
|
+
this.cachedResult = {
|
|
686
|
+
visitorId,
|
|
687
|
+
confidence,
|
|
688
|
+
components,
|
|
689
|
+
};
|
|
690
|
+
return this.cachedResult;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Clear the cached result to force re-fingerprinting
|
|
694
|
+
*/
|
|
695
|
+
clearCache() {
|
|
696
|
+
this.cachedResult = null;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Collect all fingerprint components
|
|
700
|
+
*/
|
|
701
|
+
async collectComponents() {
|
|
702
|
+
const [canvas, webgl, audio, adBlockerDetected, incognitoDetected,] = await Promise.all([
|
|
703
|
+
this.config.enableCanvas ? this.getCanvasFingerprint() : { hash: '' },
|
|
704
|
+
this.config.enableWebGL ? this.getWebGLFingerprint() : { renderer: '', vendor: '', hash: '' },
|
|
705
|
+
this.config.enableAudio ? this.getAudioFingerprint() : { hash: '' },
|
|
706
|
+
this.detectAdBlocker(),
|
|
707
|
+
this.detectIncognito(),
|
|
708
|
+
]);
|
|
709
|
+
const fonts = this.config.enableFonts ? this.detectFonts() : [];
|
|
710
|
+
const hardware = this.config.enableHardware ? this.getHardwareInfo() : {
|
|
711
|
+
concurrency: 0,
|
|
712
|
+
deviceMemory: 0,
|
|
713
|
+
maxTouchPoints: 0,
|
|
714
|
+
platform: '',
|
|
715
|
+
};
|
|
716
|
+
const screen = this.getScreenInfo();
|
|
717
|
+
const browser = this.getBrowserInfo();
|
|
718
|
+
return {
|
|
719
|
+
canvas,
|
|
720
|
+
webgl,
|
|
721
|
+
audio,
|
|
722
|
+
fonts,
|
|
723
|
+
hardware,
|
|
724
|
+
screen,
|
|
725
|
+
browser,
|
|
726
|
+
adBlockerDetected,
|
|
727
|
+
incognitoDetected,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Canvas fingerprinting - draws various shapes and text, then hashes the result
|
|
732
|
+
*/
|
|
733
|
+
async getCanvasFingerprint() {
|
|
734
|
+
try {
|
|
735
|
+
const canvas = document.createElement('canvas');
|
|
736
|
+
canvas.width = 256;
|
|
737
|
+
canvas.height = 128;
|
|
738
|
+
const ctx = canvas.getContext('2d');
|
|
739
|
+
if (!ctx) {
|
|
740
|
+
return { hash: '' };
|
|
741
|
+
}
|
|
742
|
+
// Draw background
|
|
743
|
+
ctx.fillStyle = '#f8f8f8';
|
|
744
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
745
|
+
// Draw gradient
|
|
746
|
+
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
|
747
|
+
gradient.addColorStop(0, '#ff6b6b');
|
|
748
|
+
gradient.addColorStop(0.5, '#4ecdc4');
|
|
749
|
+
gradient.addColorStop(1, '#45b7d1');
|
|
750
|
+
ctx.fillStyle = gradient;
|
|
751
|
+
ctx.fillRect(10, 10, 100, 50);
|
|
752
|
+
// Draw text with different fonts
|
|
753
|
+
ctx.font = '18px Arial';
|
|
754
|
+
ctx.fillStyle = '#333';
|
|
755
|
+
ctx.textBaseline = 'alphabetic';
|
|
756
|
+
ctx.fillText('Seekora Fingerprint 🔐', 20, 90);
|
|
757
|
+
// Draw more text with different styling
|
|
758
|
+
ctx.font = 'bold 14px Georgia';
|
|
759
|
+
ctx.fillStyle = 'rgba(102, 204, 153, 0.7)';
|
|
760
|
+
ctx.fillText('Test String @#$%', 130, 40);
|
|
761
|
+
// Draw shapes
|
|
762
|
+
ctx.beginPath();
|
|
763
|
+
ctx.arc(200, 70, 30, 0, Math.PI * 2);
|
|
764
|
+
ctx.fillStyle = 'rgba(255, 165, 0, 0.5)';
|
|
765
|
+
ctx.fill();
|
|
766
|
+
ctx.strokeStyle = '#333';
|
|
767
|
+
ctx.lineWidth = 2;
|
|
768
|
+
ctx.stroke();
|
|
769
|
+
// Draw bezier curve
|
|
770
|
+
ctx.beginPath();
|
|
771
|
+
ctx.moveTo(10, 120);
|
|
772
|
+
ctx.bezierCurveTo(50, 80, 100, 120, 140, 100);
|
|
773
|
+
ctx.strokeStyle = '#9b59b6';
|
|
774
|
+
ctx.lineWidth = 3;
|
|
775
|
+
ctx.stroke();
|
|
776
|
+
// Draw rotated rectangle
|
|
777
|
+
ctx.save();
|
|
778
|
+
ctx.translate(180, 100);
|
|
779
|
+
ctx.rotate(0.5);
|
|
780
|
+
ctx.fillStyle = 'rgba(52, 152, 219, 0.6)';
|
|
781
|
+
ctx.fillRect(-20, -10, 40, 20);
|
|
782
|
+
ctx.restore();
|
|
783
|
+
const data = canvas.toDataURL('image/png');
|
|
784
|
+
const hash = murmurHash3Hex(data);
|
|
785
|
+
return { hash, data };
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
return { hash: '' };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* WebGL fingerprinting - extracts GPU info and renders test triangles
|
|
793
|
+
*/
|
|
794
|
+
async getWebGLFingerprint() {
|
|
795
|
+
try {
|
|
796
|
+
const canvas = document.createElement('canvas');
|
|
797
|
+
canvas.width = 256;
|
|
798
|
+
canvas.height = 256;
|
|
799
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
800
|
+
if (!gl) {
|
|
801
|
+
return { renderer: '', vendor: '', hash: '' };
|
|
802
|
+
}
|
|
803
|
+
// Get debug info extension for renderer/vendor
|
|
804
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
805
|
+
const renderer = debugInfo
|
|
806
|
+
? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || ''
|
|
807
|
+
: '';
|
|
808
|
+
const vendor = debugInfo
|
|
809
|
+
? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || ''
|
|
810
|
+
: '';
|
|
811
|
+
// Collect WebGL parameters
|
|
812
|
+
const params = [];
|
|
813
|
+
// Basic parameters
|
|
814
|
+
const paramIds = [
|
|
815
|
+
gl.ALIASED_LINE_WIDTH_RANGE,
|
|
816
|
+
gl.ALIASED_POINT_SIZE_RANGE,
|
|
817
|
+
gl.ALPHA_BITS,
|
|
818
|
+
gl.BLUE_BITS,
|
|
819
|
+
gl.DEPTH_BITS,
|
|
820
|
+
gl.GREEN_BITS,
|
|
821
|
+
gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS,
|
|
822
|
+
gl.MAX_CUBE_MAP_TEXTURE_SIZE,
|
|
823
|
+
gl.MAX_FRAGMENT_UNIFORM_VECTORS,
|
|
824
|
+
gl.MAX_RENDERBUFFER_SIZE,
|
|
825
|
+
gl.MAX_TEXTURE_IMAGE_UNITS,
|
|
826
|
+
gl.MAX_TEXTURE_SIZE,
|
|
827
|
+
gl.MAX_VARYING_VECTORS,
|
|
828
|
+
gl.MAX_VERTEX_ATTRIBS,
|
|
829
|
+
gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS,
|
|
830
|
+
gl.MAX_VERTEX_UNIFORM_VECTORS,
|
|
831
|
+
gl.MAX_VIEWPORT_DIMS,
|
|
832
|
+
gl.RED_BITS,
|
|
833
|
+
gl.RENDERER,
|
|
834
|
+
gl.SHADING_LANGUAGE_VERSION,
|
|
835
|
+
gl.STENCIL_BITS,
|
|
836
|
+
gl.VENDOR,
|
|
837
|
+
gl.VERSION,
|
|
838
|
+
];
|
|
839
|
+
for (const paramId of paramIds) {
|
|
840
|
+
try {
|
|
841
|
+
const value = gl.getParameter(paramId);
|
|
842
|
+
if (value !== null && value !== undefined) {
|
|
843
|
+
if (value instanceof Float32Array || value instanceof Int32Array) {
|
|
844
|
+
params.push(Array.from(value).join(','));
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
params.push(String(value));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
// Parameter not available
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// Get supported extensions
|
|
856
|
+
const extensions = gl.getSupportedExtensions() || [];
|
|
857
|
+
params.push(extensions.sort().join(','));
|
|
858
|
+
// Render a test triangle and get pixel data
|
|
859
|
+
try {
|
|
860
|
+
const vertexShaderSource = `
|
|
861
|
+
attribute vec2 position;
|
|
862
|
+
void main() {
|
|
863
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
864
|
+
}
|
|
865
|
+
`;
|
|
866
|
+
const fragmentShaderSource = `
|
|
867
|
+
precision mediump float;
|
|
868
|
+
void main() {
|
|
869
|
+
gl_FragColor = vec4(0.812, 0.373, 0.784, 1.0);
|
|
870
|
+
}
|
|
871
|
+
`;
|
|
872
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
873
|
+
gl.shaderSource(vertexShader, vertexShaderSource);
|
|
874
|
+
gl.compileShader(vertexShader);
|
|
875
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
876
|
+
gl.shaderSource(fragmentShader, fragmentShaderSource);
|
|
877
|
+
gl.compileShader(fragmentShader);
|
|
878
|
+
const program = gl.createProgram();
|
|
879
|
+
gl.attachShader(program, vertexShader);
|
|
880
|
+
gl.attachShader(program, fragmentShader);
|
|
881
|
+
gl.linkProgram(program);
|
|
882
|
+
gl.useProgram(program);
|
|
883
|
+
const vertices = new Float32Array([
|
|
884
|
+
0.0, 0.5,
|
|
885
|
+
-0.5, -0.5,
|
|
886
|
+
0.5, -0.5,
|
|
887
|
+
]);
|
|
888
|
+
const buffer = gl.createBuffer();
|
|
889
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
890
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
891
|
+
const positionLocation = gl.getAttribLocation(program, 'position');
|
|
892
|
+
gl.enableVertexAttribArray(positionLocation);
|
|
893
|
+
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
894
|
+
gl.clearColor(0.1, 0.1, 0.1, 1.0);
|
|
895
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
896
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
897
|
+
const pixels = new Uint8Array(256 * 256 * 4);
|
|
898
|
+
gl.readPixels(0, 0, 256, 256, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
899
|
+
// Sample some pixels for the hash
|
|
900
|
+
const sampleIndices = [0, 1000, 5000, 10000, 25000, 50000, 100000, 200000];
|
|
901
|
+
for (const idx of sampleIndices) {
|
|
902
|
+
if (idx < pixels.length) {
|
|
903
|
+
params.push(String(pixels[idx]));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
// Shader compilation/rendering failed
|
|
909
|
+
}
|
|
910
|
+
const dataString = `${renderer}|${vendor}|${params.join('|')}`;
|
|
911
|
+
const hash = murmurHash3Hex(dataString);
|
|
912
|
+
return { renderer, vendor, hash };
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
return { renderer: '', vendor: '', hash: '' };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Audio fingerprinting using AudioContext oscillator
|
|
920
|
+
*/
|
|
921
|
+
async getAudioFingerprint() {
|
|
922
|
+
try {
|
|
923
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
924
|
+
if (!AudioContext) {
|
|
925
|
+
return { hash: '' };
|
|
926
|
+
}
|
|
927
|
+
const context = new AudioContext();
|
|
928
|
+
// Create oscillator
|
|
929
|
+
const oscillator = context.createOscillator();
|
|
930
|
+
oscillator.type = 'triangle';
|
|
931
|
+
oscillator.frequency.setValueAtTime(10000, context.currentTime);
|
|
932
|
+
// Create compressor
|
|
933
|
+
const compressor = context.createDynamicsCompressor();
|
|
934
|
+
compressor.threshold.setValueAtTime(-50, context.currentTime);
|
|
935
|
+
compressor.knee.setValueAtTime(40, context.currentTime);
|
|
936
|
+
compressor.ratio.setValueAtTime(12, context.currentTime);
|
|
937
|
+
compressor.attack.setValueAtTime(0, context.currentTime);
|
|
938
|
+
compressor.release.setValueAtTime(0.25, context.currentTime);
|
|
939
|
+
// Create analyser
|
|
940
|
+
const analyser = context.createAnalyser();
|
|
941
|
+
analyser.fftSize = 2048;
|
|
942
|
+
// Connect nodes
|
|
943
|
+
oscillator.connect(compressor);
|
|
944
|
+
compressor.connect(analyser);
|
|
945
|
+
analyser.connect(context.destination);
|
|
946
|
+
// Start and get data
|
|
947
|
+
oscillator.start(0);
|
|
948
|
+
return new Promise((resolve) => {
|
|
949
|
+
const timeout = setTimeout(() => {
|
|
950
|
+
try {
|
|
951
|
+
oscillator.stop();
|
|
952
|
+
context.close();
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
// Ignore cleanup errors
|
|
956
|
+
}
|
|
957
|
+
resolve({ hash: '' });
|
|
958
|
+
}, this.config.timeout);
|
|
959
|
+
// Give it a moment to process
|
|
960
|
+
setTimeout(() => {
|
|
961
|
+
try {
|
|
962
|
+
const dataArray = new Float32Array(analyser.frequencyBinCount);
|
|
963
|
+
analyser.getFloatFrequencyData(dataArray);
|
|
964
|
+
// Calculate a fingerprint value from the audio data
|
|
965
|
+
let sum = 0;
|
|
966
|
+
let count = 0;
|
|
967
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
968
|
+
if (isFinite(dataArray[i]) && dataArray[i] !== 0) {
|
|
969
|
+
sum += Math.abs(dataArray[i]);
|
|
970
|
+
count++;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const value = count > 0 ? sum / count : 0;
|
|
974
|
+
const dataString = Array.from(dataArray.slice(0, 100))
|
|
975
|
+
.filter(v => isFinite(v))
|
|
976
|
+
.map(v => v.toFixed(4))
|
|
977
|
+
.join(',');
|
|
978
|
+
clearTimeout(timeout);
|
|
979
|
+
oscillator.stop();
|
|
980
|
+
context.close();
|
|
981
|
+
const hash = murmurHash3Hex(dataString + String(value));
|
|
982
|
+
resolve({ hash, value });
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
clearTimeout(timeout);
|
|
986
|
+
try {
|
|
987
|
+
oscillator.stop();
|
|
988
|
+
context.close();
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
// Ignore cleanup errors
|
|
992
|
+
}
|
|
993
|
+
resolve({ hash: '' });
|
|
994
|
+
}
|
|
995
|
+
}, 200);
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
return { hash: '' };
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Font detection by measuring text rendering differences
|
|
1004
|
+
*/
|
|
1005
|
+
detectFonts() {
|
|
1006
|
+
const detectedFonts = [];
|
|
1007
|
+
try {
|
|
1008
|
+
const baseFonts = ['monospace', 'sans-serif', 'serif'];
|
|
1009
|
+
const testString = 'mmmmmmmmmmlli';
|
|
1010
|
+
const testSize = '72px';
|
|
1011
|
+
const canvas = document.createElement('canvas');
|
|
1012
|
+
canvas.width = 500;
|
|
1013
|
+
canvas.height = 100;
|
|
1014
|
+
const ctx = canvas.getContext('2d');
|
|
1015
|
+
if (!ctx) {
|
|
1016
|
+
return detectedFonts;
|
|
1017
|
+
}
|
|
1018
|
+
// Get base widths
|
|
1019
|
+
const baseWidths = {};
|
|
1020
|
+
for (const baseFont of baseFonts) {
|
|
1021
|
+
ctx.font = `${testSize} ${baseFont}`;
|
|
1022
|
+
baseWidths[baseFont] = ctx.measureText(testString).width;
|
|
1023
|
+
}
|
|
1024
|
+
// Check each font
|
|
1025
|
+
for (const font of FONTS_TO_CHECK) {
|
|
1026
|
+
let detected = false;
|
|
1027
|
+
for (const baseFont of baseFonts) {
|
|
1028
|
+
ctx.font = `${testSize} "${font}", ${baseFont}`;
|
|
1029
|
+
const width = ctx.measureText(testString).width;
|
|
1030
|
+
if (width !== baseWidths[baseFont]) {
|
|
1031
|
+
detected = true;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (detected) {
|
|
1036
|
+
detectedFonts.push(font);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
// Font detection failed
|
|
1042
|
+
}
|
|
1043
|
+
return detectedFonts;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Get hardware information
|
|
1047
|
+
*/
|
|
1048
|
+
getHardwareInfo() {
|
|
1049
|
+
const nav = navigator;
|
|
1050
|
+
return {
|
|
1051
|
+
concurrency: nav.hardwareConcurrency || 0,
|
|
1052
|
+
deviceMemory: nav.deviceMemory || 0,
|
|
1053
|
+
maxTouchPoints: nav.maxTouchPoints || 0,
|
|
1054
|
+
platform: nav.platform || '',
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Get screen information
|
|
1059
|
+
*/
|
|
1060
|
+
getScreenInfo() {
|
|
1061
|
+
return {
|
|
1062
|
+
width: screen.width || 0,
|
|
1063
|
+
height: screen.height || 0,
|
|
1064
|
+
colorDepth: screen.colorDepth || 0,
|
|
1065
|
+
pixelRatio: window.devicePixelRatio || 1,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Get browser information
|
|
1070
|
+
*/
|
|
1071
|
+
getBrowserInfo() {
|
|
1072
|
+
const nav = navigator;
|
|
1073
|
+
return {
|
|
1074
|
+
language: nav.language || '',
|
|
1075
|
+
languages: Array.from(nav.languages || []),
|
|
1076
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
|
|
1077
|
+
timezoneOffset: new Date().getTimezoneOffset(),
|
|
1078
|
+
cookieEnabled: nav.cookieEnabled ?? false,
|
|
1079
|
+
doNotTrack: nav.doNotTrack === '1' || nav.doNotTrack === 'yes',
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Detect if an ad blocker is installed
|
|
1084
|
+
*/
|
|
1085
|
+
async detectAdBlocker() {
|
|
1086
|
+
try {
|
|
1087
|
+
// Create a bait element
|
|
1088
|
+
const bait = document.createElement('div');
|
|
1089
|
+
bait.className = 'adsbox ad-banner ad_banner textAd text_ad text-ad';
|
|
1090
|
+
bait.style.cssText = 'position: absolute; left: -9999px; width: 1px; height: 1px;';
|
|
1091
|
+
bait.innerHTML = ' ';
|
|
1092
|
+
document.body.appendChild(bait);
|
|
1093
|
+
// Wait a bit for ad blockers to act
|
|
1094
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1095
|
+
const isBlocked = bait.offsetParent === null ||
|
|
1096
|
+
bait.offsetHeight === 0 ||
|
|
1097
|
+
bait.offsetWidth === 0 ||
|
|
1098
|
+
getComputedStyle(bait).display === 'none' ||
|
|
1099
|
+
getComputedStyle(bait).visibility === 'hidden';
|
|
1100
|
+
document.body.removeChild(bait);
|
|
1101
|
+
// Also try to fetch a known ad-related resource
|
|
1102
|
+
try {
|
|
1103
|
+
const response = await fetch('https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js', {
|
|
1104
|
+
method: 'HEAD',
|
|
1105
|
+
mode: 'no-cors',
|
|
1106
|
+
});
|
|
1107
|
+
// If we get here without error, no ad blocker (though no-cors doesn't give us much info)
|
|
1108
|
+
return isBlocked;
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
return true; // Blocked
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Detect if browser is in incognito/private mode
|
|
1120
|
+
*/
|
|
1121
|
+
async detectIncognito() {
|
|
1122
|
+
// Chrome/Chromium detection
|
|
1123
|
+
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
|
1124
|
+
try {
|
|
1125
|
+
const estimate = await navigator.storage.estimate();
|
|
1126
|
+
// In incognito, quota is typically limited to ~120MB
|
|
1127
|
+
if (estimate.quota && estimate.quota < 130 * 1024 * 1024) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
// Storage API not available
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// Firefox detection using IndexedDB
|
|
1136
|
+
try {
|
|
1137
|
+
const db = indexedDB.open('test-private-mode');
|
|
1138
|
+
return new Promise((resolve) => {
|
|
1139
|
+
db.onerror = () => resolve(true);
|
|
1140
|
+
db.onsuccess = () => {
|
|
1141
|
+
db.result.close();
|
|
1142
|
+
indexedDB.deleteDatabase('test-private-mode');
|
|
1143
|
+
resolve(false);
|
|
1144
|
+
};
|
|
1145
|
+
// Timeout fallback
|
|
1146
|
+
setTimeout(() => resolve(false), 500);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
// IndexedDB not available - might be incognito
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Generate a hash from any data
|
|
1156
|
+
*/
|
|
1157
|
+
generateHash(data) {
|
|
1158
|
+
return murmurHash3Hex(data);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Generate the final visitor ID from all components
|
|
1162
|
+
*/
|
|
1163
|
+
generateVisitorId(components) {
|
|
1164
|
+
const data = JSON.stringify({
|
|
1165
|
+
canvas: components.canvas.hash,
|
|
1166
|
+
webgl: components.webgl.hash,
|
|
1167
|
+
audio: components.audio.hash,
|
|
1168
|
+
fonts: components.fonts.sort().join(','),
|
|
1169
|
+
hardware: components.hardware,
|
|
1170
|
+
screen: components.screen,
|
|
1171
|
+
browser: {
|
|
1172
|
+
...components.browser,
|
|
1173
|
+
languages: components.browser.languages.sort().join(','),
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
// Generate a 32-character hex string (similar to UUID format)
|
|
1177
|
+
const hash1 = murmurHash3Hex(data, 0);
|
|
1178
|
+
const hash2 = murmurHash3Hex(data, 1);
|
|
1179
|
+
const hash3 = murmurHash3Hex(data, 2);
|
|
1180
|
+
const hash4 = murmurHash3Hex(data, 3);
|
|
1181
|
+
return `${hash1}${hash2}${hash3}${hash4}`;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Calculate confidence score based on available components
|
|
1185
|
+
*/
|
|
1186
|
+
calculateConfidence(components) {
|
|
1187
|
+
let score = 0;
|
|
1188
|
+
let maxScore = 0;
|
|
1189
|
+
// Canvas (high entropy)
|
|
1190
|
+
maxScore += 0.2;
|
|
1191
|
+
if (components.canvas.hash) {
|
|
1192
|
+
score += 0.2;
|
|
1193
|
+
}
|
|
1194
|
+
// WebGL (high entropy)
|
|
1195
|
+
maxScore += 0.2;
|
|
1196
|
+
if (components.webgl.hash && components.webgl.renderer) {
|
|
1197
|
+
score += 0.2;
|
|
1198
|
+
}
|
|
1199
|
+
else if (components.webgl.hash) {
|
|
1200
|
+
score += 0.1;
|
|
1201
|
+
}
|
|
1202
|
+
// Audio (medium entropy)
|
|
1203
|
+
maxScore += 0.15;
|
|
1204
|
+
if (components.audio.hash) {
|
|
1205
|
+
score += 0.15;
|
|
1206
|
+
}
|
|
1207
|
+
// Fonts (high entropy)
|
|
1208
|
+
maxScore += 0.15;
|
|
1209
|
+
if (components.fonts.length > 10) {
|
|
1210
|
+
score += 0.15;
|
|
1211
|
+
}
|
|
1212
|
+
else if (components.fonts.length > 5) {
|
|
1213
|
+
score += 0.1;
|
|
1214
|
+
}
|
|
1215
|
+
else if (components.fonts.length > 0) {
|
|
1216
|
+
score += 0.05;
|
|
1217
|
+
}
|
|
1218
|
+
// Hardware (medium entropy)
|
|
1219
|
+
maxScore += 0.1;
|
|
1220
|
+
if (components.hardware.concurrency > 0 && components.hardware.platform) {
|
|
1221
|
+
score += 0.1;
|
|
1222
|
+
}
|
|
1223
|
+
else if (components.hardware.platform) {
|
|
1224
|
+
score += 0.05;
|
|
1225
|
+
}
|
|
1226
|
+
// Screen (low entropy but stable)
|
|
1227
|
+
maxScore += 0.1;
|
|
1228
|
+
if (components.screen.width > 0 && components.screen.height > 0) {
|
|
1229
|
+
score += 0.1;
|
|
1230
|
+
}
|
|
1231
|
+
// Browser (low entropy but stable)
|
|
1232
|
+
maxScore += 0.1;
|
|
1233
|
+
if (components.browser.timezone && components.browser.language) {
|
|
1234
|
+
score += 0.1;
|
|
1235
|
+
}
|
|
1236
|
+
else if (components.browser.language) {
|
|
1237
|
+
score += 0.05;
|
|
1238
|
+
}
|
|
1239
|
+
// Normalize and return
|
|
1240
|
+
return Math.min(1, score / maxScore);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
// ============================================================================
|
|
1244
|
+
// Convenience Function
|
|
1245
|
+
// ============================================================================
|
|
1246
|
+
/**
|
|
1247
|
+
* Quick fingerprint getter - creates a Fingerprint instance and returns the result
|
|
1248
|
+
*/
|
|
1249
|
+
async function getFingerprint(config) {
|
|
1250
|
+
const fingerprint = new Fingerprint(config);
|
|
1251
|
+
return fingerprint.get();
|
|
1252
|
+
}
|
|
1253
|
+
|
|
570
1254
|
/**
|
|
571
1255
|
* Default Theme
|
|
572
1256
|
*/
|
|
@@ -1135,38 +1819,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
1135
1819
|
const state = { currentPage, itemsPerPage: stateItemsPerPage };
|
|
1136
1820
|
// Calculate absolute position (1-based) accounting for pagination
|
|
1137
1821
|
const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
hasContext: !!searchContext,
|
|
1150
|
-
});
|
|
1151
|
-
await client.trackEvent?.({
|
|
1152
|
-
event_name: 'product_click',
|
|
1153
|
-
clicked_item_id: result.id,
|
|
1154
|
-
metadata: {
|
|
1155
|
-
result: result,
|
|
1156
|
-
position: absolutePosition,
|
|
1157
|
-
},
|
|
1158
|
-
}, searchContext);
|
|
1159
|
-
console.log('🟢 SearchResults: Analytics event tracked successfully', {
|
|
1160
|
-
resultId: result.id,
|
|
1161
|
-
position: absolutePosition,
|
|
1162
|
-
});
|
|
1822
|
+
const searchContext = results?.context;
|
|
1823
|
+
if (client.trackClick) {
|
|
1824
|
+
await client.trackClick(result.id, absolutePosition, searchContext);
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
await client.trackEvent?.({
|
|
1828
|
+
event_name: 'product_click',
|
|
1829
|
+
clicked_item_id: result.id,
|
|
1830
|
+
metadata: { result, position: absolutePosition },
|
|
1831
|
+
}, searchContext);
|
|
1832
|
+
}
|
|
1163
1833
|
}
|
|
1164
1834
|
catch (err) {
|
|
1165
1835
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1166
|
-
|
|
1167
|
-
resultId: result.id,
|
|
1168
|
-
error: error.message,
|
|
1169
|
-
});
|
|
1836
|
+
log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
|
|
1170
1837
|
}
|
|
1171
1838
|
}
|
|
1172
1839
|
// Call user-provided callback
|
|
@@ -1226,38 +1893,21 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
1226
1893
|
const state = { currentPage, itemsPerPage: stateItemsPerPage };
|
|
1227
1894
|
// Calculate absolute position (1-based) accounting for pagination
|
|
1228
1895
|
const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + index + 1;
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
hasContext: !!searchContext,
|
|
1241
|
-
});
|
|
1242
|
-
await client.trackEvent?.({
|
|
1243
|
-
event_name: 'product_click',
|
|
1244
|
-
clicked_item_id: result.id,
|
|
1245
|
-
metadata: {
|
|
1246
|
-
result: result,
|
|
1247
|
-
position: absolutePosition,
|
|
1248
|
-
},
|
|
1249
|
-
}, searchContext);
|
|
1250
|
-
console.log('🟢 SearchResults: Analytics event tracked successfully', {
|
|
1251
|
-
resultId: result.id,
|
|
1252
|
-
position: absolutePosition,
|
|
1253
|
-
});
|
|
1896
|
+
const searchContext = results?.context;
|
|
1897
|
+
if (client.trackClick) {
|
|
1898
|
+
await client.trackClick(result.id, absolutePosition, searchContext);
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
await client.trackEvent?.({
|
|
1902
|
+
event_name: 'product_click',
|
|
1903
|
+
clicked_item_id: result.id,
|
|
1904
|
+
metadata: { result, position: absolutePosition },
|
|
1905
|
+
}, searchContext);
|
|
1906
|
+
}
|
|
1254
1907
|
}
|
|
1255
1908
|
catch (err) {
|
|
1256
1909
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1257
|
-
|
|
1258
|
-
resultId: result.id,
|
|
1259
|
-
error: error.message,
|
|
1260
|
-
});
|
|
1910
|
+
log.error('SearchResults: Error tracking click', { resultId: result.id, error: error.message });
|
|
1261
1911
|
}
|
|
1262
1912
|
}
|
|
1263
1913
|
// Call user-provided callback
|
|
@@ -2615,7 +3265,7 @@ const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, s
|
|
|
2615
3265
|
*
|
|
2616
3266
|
* Renders highlighted search result text with matching terms emphasized
|
|
2617
3267
|
*/
|
|
2618
|
-
const Highlight = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
|
|
3268
|
+
const Highlight$1 = ({ hit, attribute, className, style, theme: customTheme, tagName: Tag = 'span', renderHighlighted, renderNonHighlighted, query, }) => {
|
|
2619
3269
|
const { theme } = useSearchContext();
|
|
2620
3270
|
const highlightTheme = customTheme || {};
|
|
2621
3271
|
// Get highlighted value from hit
|
|
@@ -5589,7 +6239,7 @@ const EVENTS = {
|
|
|
5589
6239
|
// Hook Implementation
|
|
5590
6240
|
// ============================================================================
|
|
5591
6241
|
function useSuggestionsAnalytics(options) {
|
|
5592
|
-
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;
|
|
5593
6243
|
// Refs for debouncing and tracking
|
|
5594
6244
|
const impressionTimerRef = React.useRef(null);
|
|
5595
6245
|
const lastImpressionRef = React.useRef(null);
|
|
@@ -5602,10 +6252,11 @@ function useSuggestionsAnalytics(options) {
|
|
|
5602
6252
|
}
|
|
5603
6253
|
};
|
|
5604
6254
|
}, []);
|
|
5605
|
-
// Helper to send event
|
|
5606
|
-
const sendEvent = React.useCallback(async (eventName, metadata) => {
|
|
6255
|
+
// Helper to send event (optional context links event to search for v3 analytics)
|
|
6256
|
+
const sendEvent = React.useCallback(async (eventName, metadata, context) => {
|
|
5607
6257
|
if (!enabled || !client)
|
|
5608
6258
|
return;
|
|
6259
|
+
const searchContext = context ?? contextOption;
|
|
5609
6260
|
try {
|
|
5610
6261
|
await client.trackEvent?.({
|
|
5611
6262
|
event_name: eventName,
|
|
@@ -5615,13 +6266,13 @@ function useSuggestionsAnalytics(options) {
|
|
|
5615
6266
|
timestamp: Date.now(),
|
|
5616
6267
|
source: 'suggestions_dropdown',
|
|
5617
6268
|
},
|
|
5618
|
-
});
|
|
6269
|
+
}, searchContext);
|
|
5619
6270
|
log.verbose(`Analytics: ${eventName}`, metadata);
|
|
5620
6271
|
}
|
|
5621
6272
|
catch (error) {
|
|
5622
6273
|
log.warn(`Failed to track ${eventName}`, { error });
|
|
5623
6274
|
}
|
|
5624
|
-
}, [client, enabled, analyticsTags]);
|
|
6275
|
+
}, [client, enabled, analyticsTags, contextOption]);
|
|
5625
6276
|
// Track suggestion click
|
|
5626
6277
|
const trackSuggestionClick = React.useCallback((data) => {
|
|
5627
6278
|
if (!trackClicks)
|
|
@@ -5649,11 +6300,12 @@ function useSuggestionsAnalytics(options) {
|
|
|
5649
6300
|
tab_id: data.tabId,
|
|
5650
6301
|
original_query: data.query,
|
|
5651
6302
|
});
|
|
5652
|
-
// Also track as a general product click for analytics
|
|
5653
|
-
|
|
5654
|
-
|
|
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(() => { });
|
|
5655
6307
|
}
|
|
5656
|
-
}, [client, sendEvent, trackClicks]);
|
|
6308
|
+
}, [client, contextOption, sendEvent, trackClicks]);
|
|
5657
6309
|
// Track category click
|
|
5658
6310
|
const trackCategoryClick = React.useCallback((category, query) => {
|
|
5659
6311
|
if (!trackClicks)
|
|
@@ -10563,6 +11215,881 @@ const SuggestionDropdownVariants = {
|
|
|
10563
11215
|
minimal: MinimalDropdown,
|
|
10564
11216
|
};
|
|
10565
11217
|
|
|
11218
|
+
function Modal({ isOpen, onClose, children }) {
|
|
11219
|
+
const overlayRef = React.useRef(null);
|
|
11220
|
+
const containerRef = React.useRef(null);
|
|
11221
|
+
React.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
|
+
React.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
|
+
React.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 reactDom.createPortal(modalContent, document.body);
|
|
11269
|
+
}
|
|
11270
|
+
|
|
11271
|
+
function SearchBox({ value, onChange, onKeyDown, placeholder = 'Search documentation...', isLoading = false, onClear, }) {
|
|
11272
|
+
const inputRef = React.useRef(null);
|
|
11273
|
+
React.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
|
+
function groupHitsByHierarchy(hits) {
|
|
11442
|
+
const groups = new Map();
|
|
11443
|
+
for (const hit of hits) {
|
|
11444
|
+
const lvl0 = hit.hierarchy?.lvl0 || '';
|
|
11445
|
+
if (!groups.has(lvl0))
|
|
11446
|
+
groups.set(lvl0, []);
|
|
11447
|
+
groups.get(lvl0).push(hit);
|
|
11448
|
+
}
|
|
11449
|
+
const result = [];
|
|
11450
|
+
for (const [lvl0Key, groupHits] of groups.entries()) {
|
|
11451
|
+
const sortedHits = [...groupHits].sort((a, b) => {
|
|
11452
|
+
const aType = a.type;
|
|
11453
|
+
const bType = b.type;
|
|
11454
|
+
return getTypeLevel(aType) - getTypeLevel(bType);
|
|
11455
|
+
});
|
|
11456
|
+
const markedHits = sortedHits.map((hit, index) => {
|
|
11457
|
+
const suggestion = hit;
|
|
11458
|
+
const isChild = isChildType(suggestion.type);
|
|
11459
|
+
const nextHit = sortedHits[index + 1];
|
|
11460
|
+
const isLastChild = isChild && (!nextHit || !isChildType(nextHit.type));
|
|
11461
|
+
return { ...hit, isChild, isLastChild };
|
|
11462
|
+
});
|
|
11463
|
+
result.push({ category: lvl0Key || null, hits: markedHits });
|
|
11464
|
+
}
|
|
11465
|
+
return result;
|
|
11466
|
+
}
|
|
11467
|
+
function getGlobalIndexMulti(groups, groupIndex, hitIndex) {
|
|
11468
|
+
let index = 0;
|
|
11469
|
+
for (let i = 0; i < groupIndex; i++)
|
|
11470
|
+
index += groups[i].hits.length;
|
|
11471
|
+
return index + hitIndex;
|
|
11472
|
+
}
|
|
11473
|
+
function getHitKey(hit, index) {
|
|
11474
|
+
if ('objectID' in hit)
|
|
11475
|
+
return hit.objectID;
|
|
11476
|
+
return `suggestion-${hit.url}-${index}`;
|
|
11477
|
+
}
|
|
11478
|
+
function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, query, isLoading, error, translations = {}, sources: _sources = [], }) {
|
|
11479
|
+
const listRef = React.useRef(null);
|
|
11480
|
+
React.useEffect(() => {
|
|
11481
|
+
if (listRef.current && hits.length > 0) {
|
|
11482
|
+
const selectedItem = listRef.current.children[selectedIndex];
|
|
11483
|
+
if (selectedItem)
|
|
11484
|
+
selectedItem.scrollIntoView({ block: 'nearest' });
|
|
11485
|
+
}
|
|
11486
|
+
}, [selectedIndex, hits.length]);
|
|
11487
|
+
if (!query) {
|
|
11488
|
+
return (React.createElement("div", { className: "seekora-docsearch-empty" },
|
|
11489
|
+
React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
|
|
11490
|
+
}
|
|
11491
|
+
if (isLoading && hits.length === 0) {
|
|
11492
|
+
return (React.createElement("div", { className: "seekora-docsearch-loading" },
|
|
11493
|
+
React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
|
|
11494
|
+
React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
11495
|
+
React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "50", strokeDashoffset: "15" },
|
|
11496
|
+
React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
|
|
11497
|
+
React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
|
|
11498
|
+
}
|
|
11499
|
+
if (error) {
|
|
11500
|
+
return (React.createElement("div", { className: "seekora-docsearch-error" },
|
|
11501
|
+
React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
|
|
11502
|
+
}
|
|
11503
|
+
if (hits.length === 0 && query) {
|
|
11504
|
+
return (React.createElement("div", { className: "seekora-docsearch-no-results" },
|
|
11505
|
+
React.createElement("p", { className: "seekora-docsearch-no-results-text" }, translations.noResultsText || `No results found for "${query}"`)));
|
|
11506
|
+
}
|
|
11507
|
+
const displayGroups = groupedHits && groupedHits.length > 0
|
|
11508
|
+
? groupedHits.map(g => ({ category: g.source.name, sourceId: g.source.id, openInNewTab: g.source.openInNewTab, hits: g.items }))
|
|
11509
|
+
: groupHitsByHierarchy(hits).map(g => ({ category: g.category, sourceId: 'default', openInNewTab: false, hits: g.hits }));
|
|
11510
|
+
return (React.createElement("div", { className: "seekora-docsearch-results" },
|
|
11511
|
+
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" },
|
|
11512
|
+
group.category && React.createElement("div", { className: "seekora-docsearch-results-group-header" }, group.category),
|
|
11513
|
+
React.createElement("ul", { className: "seekora-docsearch-results-group-items" }, group.hits.map((hit, hitIndex) => {
|
|
11514
|
+
const globalIndex = getGlobalIndexMulti(displayGroups, groupIndex, hitIndex);
|
|
11515
|
+
const extHit = hit;
|
|
11516
|
+
return (React.createElement("li", { key: getHitKey(hit, hitIndex) },
|
|
11517
|
+
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 })));
|
|
11518
|
+
}))))))));
|
|
11519
|
+
}
|
|
11520
|
+
|
|
11521
|
+
function Footer({ translations = {} }) {
|
|
11522
|
+
return (React.createElement("footer", { className: "seekora-docsearch-footer" },
|
|
11523
|
+
React.createElement("div", { className: "seekora-docsearch-footer-commands" },
|
|
11524
|
+
React.createElement("ul", { className: "seekora-docsearch-footer-commands-list" },
|
|
11525
|
+
React.createElement("li", null,
|
|
11526
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11527
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u21B5"),
|
|
11528
|
+
React.createElement("span", null, "to select"))),
|
|
11529
|
+
React.createElement("li", null,
|
|
11530
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11531
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2191"),
|
|
11532
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "\u2193"),
|
|
11533
|
+
React.createElement("span", null, "to navigate"))),
|
|
11534
|
+
React.createElement("li", null,
|
|
11535
|
+
React.createElement("span", { className: "seekora-docsearch-footer-command" },
|
|
11536
|
+
React.createElement("kbd", { className: "seekora-docsearch-key" }, "esc"),
|
|
11537
|
+
React.createElement("span", null, translations.closeText || 'to close'))))),
|
|
11538
|
+
React.createElement("div", { className: "seekora-docsearch-footer-logo" },
|
|
11539
|
+
React.createElement("span", { className: "seekora-docsearch-footer-logo-text" }, translations.searchByText || 'Search by'),
|
|
11540
|
+
React.createElement("a", { href: "https://seekora.ai", target: "_blank", rel: "noopener noreferrer", className: "seekora-docsearch-footer-logo-link" },
|
|
11541
|
+
React.createElement("span", { className: "seekora-docsearch-logo", style: { fontFamily: 'system-ui', fontSize: 14, fontWeight: 600 } }, "Seekora")))));
|
|
11542
|
+
}
|
|
11543
|
+
|
|
11544
|
+
function useKeyboard(options) {
|
|
11545
|
+
const { isOpen, onOpen, onClose, onSelectNext, onSelectPrev, onEnter, disableShortcut = false, shortcutKey = 'k', } = options;
|
|
11546
|
+
const handleGlobalKeyDown = React.useCallback((event) => {
|
|
11547
|
+
if (!disableShortcut && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey) {
|
|
11548
|
+
event.preventDefault();
|
|
11549
|
+
if (isOpen) {
|
|
11550
|
+
onClose();
|
|
11551
|
+
}
|
|
11552
|
+
else {
|
|
11553
|
+
onOpen();
|
|
11554
|
+
}
|
|
11555
|
+
return;
|
|
11556
|
+
}
|
|
11557
|
+
if (!disableShortcut && event.key === '/' && !isOpen) {
|
|
11558
|
+
const target = event.target;
|
|
11559
|
+
const isInput = target.tagName === 'INPUT' ||
|
|
11560
|
+
target.tagName === 'TEXTAREA' ||
|
|
11561
|
+
target.isContentEditable;
|
|
11562
|
+
if (!isInput) {
|
|
11563
|
+
event.preventDefault();
|
|
11564
|
+
onOpen();
|
|
11565
|
+
}
|
|
11566
|
+
}
|
|
11567
|
+
}, [isOpen, onOpen, onClose, disableShortcut, shortcutKey]);
|
|
11568
|
+
const handleModalKeyDown = React.useCallback((event) => {
|
|
11569
|
+
switch (event.key) {
|
|
11570
|
+
case 'Escape':
|
|
11571
|
+
event.preventDefault();
|
|
11572
|
+
onClose();
|
|
11573
|
+
break;
|
|
11574
|
+
case 'ArrowDown':
|
|
11575
|
+
event.preventDefault();
|
|
11576
|
+
onSelectNext();
|
|
11577
|
+
break;
|
|
11578
|
+
case 'ArrowUp':
|
|
11579
|
+
event.preventDefault();
|
|
11580
|
+
onSelectPrev();
|
|
11581
|
+
break;
|
|
11582
|
+
case 'Enter':
|
|
11583
|
+
event.preventDefault();
|
|
11584
|
+
onEnter();
|
|
11585
|
+
break;
|
|
11586
|
+
case 'Tab':
|
|
11587
|
+
if (event.shiftKey) {
|
|
11588
|
+
onSelectPrev();
|
|
11589
|
+
}
|
|
11590
|
+
else {
|
|
11591
|
+
onSelectNext();
|
|
11592
|
+
}
|
|
11593
|
+
event.preventDefault();
|
|
11594
|
+
break;
|
|
11595
|
+
}
|
|
11596
|
+
}, [onClose, onSelectNext, onSelectPrev, onEnter]);
|
|
11597
|
+
React.useEffect(() => {
|
|
11598
|
+
document.addEventListener('keydown', handleGlobalKeyDown);
|
|
11599
|
+
return () => {
|
|
11600
|
+
document.removeEventListener('keydown', handleGlobalKeyDown);
|
|
11601
|
+
};
|
|
11602
|
+
}, [handleGlobalKeyDown]);
|
|
11603
|
+
return {
|
|
11604
|
+
handleModalKeyDown,
|
|
11605
|
+
};
|
|
11606
|
+
}
|
|
11607
|
+
function getShortcutText(key = 'K') {
|
|
11608
|
+
if (typeof navigator === 'undefined') {
|
|
11609
|
+
return `⌘${key}`;
|
|
11610
|
+
}
|
|
11611
|
+
const isMac = navigator.platform.toLowerCase().includes('mac');
|
|
11612
|
+
return isMac ? `⌘${key}` : `Ctrl+${key}`;
|
|
11613
|
+
}
|
|
11614
|
+
|
|
11615
|
+
function DocSearchButton({ onClick, placeholder = 'Search documentation...' }) {
|
|
11616
|
+
const shortcutText = getShortcutText('K');
|
|
11617
|
+
return (React.createElement("button", { type: "button", className: "seekora-docsearch-button", onClick: onClick, "aria-label": "Search documentation" },
|
|
11618
|
+
React.createElement("span", { className: "seekora-docsearch-button-icon" },
|
|
11619
|
+
React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
|
|
11620
|
+
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" }))),
|
|
11621
|
+
React.createElement("span", { className: "seekora-docsearch-button-placeholder" }, placeholder),
|
|
11622
|
+
React.createElement("span", { className: "seekora-docsearch-button-keys" },
|
|
11623
|
+
React.createElement("kbd", { className: "seekora-docsearch-button-key" }, shortcutText))));
|
|
11624
|
+
}
|
|
11625
|
+
|
|
11626
|
+
const initialState = {
|
|
11627
|
+
query: '',
|
|
11628
|
+
results: [],
|
|
11629
|
+
suggestions: [],
|
|
11630
|
+
groupedSuggestions: [],
|
|
11631
|
+
isLoading: false,
|
|
11632
|
+
error: null,
|
|
11633
|
+
selectedIndex: 0,
|
|
11634
|
+
mode: 'suggestions',
|
|
11635
|
+
};
|
|
11636
|
+
function reducer(state, action) {
|
|
11637
|
+
switch (action.type) {
|
|
11638
|
+
case 'SET_QUERY':
|
|
11639
|
+
return { ...state, query: action.payload, selectedIndex: 0 };
|
|
11640
|
+
case 'SET_RESULTS':
|
|
11641
|
+
return { ...state, results: action.payload, mode: 'results' };
|
|
11642
|
+
case 'SET_SUGGESTIONS':
|
|
11643
|
+
return { ...state, suggestions: action.payload, mode: 'suggestions' };
|
|
11644
|
+
case 'SET_GROUPED_SUGGESTIONS': {
|
|
11645
|
+
const flatSuggestions = action.payload.flatMap(group => group.items.map(item => ({ ...item, _source: group.source.id })));
|
|
11646
|
+
return { ...state, groupedSuggestions: action.payload, suggestions: flatSuggestions, mode: 'suggestions' };
|
|
11647
|
+
}
|
|
11648
|
+
case 'SET_LOADING':
|
|
11649
|
+
return { ...state, isLoading: action.payload };
|
|
11650
|
+
case 'SET_ERROR':
|
|
11651
|
+
return { ...state, error: action.payload };
|
|
11652
|
+
case 'SET_SELECTED_INDEX':
|
|
11653
|
+
return { ...state, selectedIndex: action.payload };
|
|
11654
|
+
case 'SELECT_NEXT': {
|
|
11655
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11656
|
+
const maxIndex = items.length - 1;
|
|
11657
|
+
return { ...state, selectedIndex: state.selectedIndex >= maxIndex ? 0 : state.selectedIndex + 1 };
|
|
11658
|
+
}
|
|
11659
|
+
case 'SELECT_PREV': {
|
|
11660
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11661
|
+
const maxIndex = items.length - 1;
|
|
11662
|
+
return { ...state, selectedIndex: state.selectedIndex <= 0 ? maxIndex : state.selectedIndex - 1 };
|
|
11663
|
+
}
|
|
11664
|
+
case 'SET_MODE':
|
|
11665
|
+
return { ...state, mode: action.payload, selectedIndex: 0 };
|
|
11666
|
+
case 'RESET':
|
|
11667
|
+
return initialState;
|
|
11668
|
+
default:
|
|
11669
|
+
return state;
|
|
11670
|
+
}
|
|
11671
|
+
}
|
|
11672
|
+
function useDocSearch(options) {
|
|
11673
|
+
const { apiEndpoint, apiKey, sources, maxResults = 10, debounceMs = 200 } = options;
|
|
11674
|
+
const searchSources = sources || (apiEndpoint ? [{
|
|
11675
|
+
id: 'default',
|
|
11676
|
+
name: 'Results',
|
|
11677
|
+
endpoint: apiEndpoint,
|
|
11678
|
+
apiKey,
|
|
11679
|
+
maxResults,
|
|
11680
|
+
}] : []);
|
|
11681
|
+
const [state, dispatch] = React.useReducer(reducer, initialState);
|
|
11682
|
+
const abortControllersRef = React.useRef(new Map());
|
|
11683
|
+
const debounceTimerRef = React.useRef(null);
|
|
11684
|
+
const defaultTransform = (data, sourceId) => {
|
|
11685
|
+
const items = data.data?.suggestions || data.data?.results || data.suggestions || data.results || data.hits || [];
|
|
11686
|
+
return items.map((item) => ({
|
|
11687
|
+
url: item.url || item.route || '',
|
|
11688
|
+
title: item.title?.replace?.(/<\/?mark>/g, '') || item.title || '',
|
|
11689
|
+
content: item.content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || item.description || '',
|
|
11690
|
+
description: item.description || item.content?.substring?.(0, 100) || '',
|
|
11691
|
+
category: item.category || item.hierarchy?.lvl0 || '',
|
|
11692
|
+
hierarchy: item.hierarchy,
|
|
11693
|
+
route: item.route,
|
|
11694
|
+
parentTitle: item.parent_title || item.parentTitle,
|
|
11695
|
+
_source: sourceId,
|
|
11696
|
+
}));
|
|
11697
|
+
};
|
|
11698
|
+
const transformPublicSearchResults = React.useCallback((data, sourceId) => {
|
|
11699
|
+
const results = data?.data?.results || [];
|
|
11700
|
+
return results.map((item) => {
|
|
11701
|
+
const doc = item.document || item;
|
|
11702
|
+
return {
|
|
11703
|
+
url: doc.url || doc.route || '',
|
|
11704
|
+
title: (doc.title || doc.name || '').replace?.(/<\/?mark>/g, '') || '',
|
|
11705
|
+
content: (doc.content || doc.description || '').replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || '',
|
|
11706
|
+
description: doc.description || doc.content?.substring?.(0, 100) || '',
|
|
11707
|
+
category: doc.category || doc.hierarchy?.lvl0 || '',
|
|
11708
|
+
hierarchy: doc.hierarchy,
|
|
11709
|
+
route: doc.route,
|
|
11710
|
+
parentTitle: doc.parent_title || doc.parentTitle,
|
|
11711
|
+
_source: sourceId,
|
|
11712
|
+
};
|
|
11713
|
+
});
|
|
11714
|
+
}, []);
|
|
11715
|
+
const fetchFromSource = React.useCallback(async (source, query, signal) => {
|
|
11716
|
+
const minLength = source.minQueryLength ?? 1;
|
|
11717
|
+
if (query.length < minLength)
|
|
11718
|
+
return [];
|
|
11719
|
+
try {
|
|
11720
|
+
if (source.storeId) {
|
|
11721
|
+
const baseUrl = source.endpoint.replace(/\/$/, '');
|
|
11722
|
+
const searchUrl = `${baseUrl}/api/v1/search`;
|
|
11723
|
+
const headers = {
|
|
11724
|
+
'Content-Type': 'application/json',
|
|
11725
|
+
'x-storeid': source.storeId,
|
|
11726
|
+
...(source.storeSecret && { 'x-storesecret': source.storeSecret }),
|
|
11727
|
+
};
|
|
11728
|
+
const response = await fetch(searchUrl, {
|
|
11729
|
+
method: 'POST',
|
|
11730
|
+
headers,
|
|
11731
|
+
body: JSON.stringify({ q: query.trim() || '*', per_page: source.maxResults || 8 }),
|
|
11732
|
+
signal,
|
|
11733
|
+
});
|
|
11734
|
+
if (!response.ok)
|
|
11735
|
+
return [];
|
|
11736
|
+
const data = await response.json();
|
|
11737
|
+
if (source.transformResults) {
|
|
11738
|
+
return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
|
|
11739
|
+
}
|
|
11740
|
+
return transformPublicSearchResults(data, source.id);
|
|
11741
|
+
}
|
|
11742
|
+
const url = new URL(source.endpoint);
|
|
11743
|
+
url.searchParams.set('query', query);
|
|
11744
|
+
url.searchParams.set('limit', String(source.maxResults || 8));
|
|
11745
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
11746
|
+
if (source.apiKey)
|
|
11747
|
+
headers['X-Docs-API-Key'] = source.apiKey;
|
|
11748
|
+
const response = await fetch(url.toString(), { method: 'GET', headers, signal });
|
|
11749
|
+
if (!response.ok)
|
|
11750
|
+
return [];
|
|
11751
|
+
const data = await response.json();
|
|
11752
|
+
if (source.transformResults) {
|
|
11753
|
+
return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
|
|
11754
|
+
}
|
|
11755
|
+
return defaultTransform(data, source.id);
|
|
11756
|
+
}
|
|
11757
|
+
catch (error) {
|
|
11758
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
11759
|
+
throw error;
|
|
11760
|
+
return [];
|
|
11761
|
+
}
|
|
11762
|
+
}, [transformPublicSearchResults]);
|
|
11763
|
+
const fetchSuggestions = React.useCallback(async (query) => {
|
|
11764
|
+
if (!query.trim()) {
|
|
11765
|
+
dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: [] });
|
|
11766
|
+
return;
|
|
11767
|
+
}
|
|
11768
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11769
|
+
abortControllersRef.current.clear();
|
|
11770
|
+
dispatch({ type: 'SET_LOADING', payload: true });
|
|
11771
|
+
dispatch({ type: 'SET_ERROR', payload: null });
|
|
11772
|
+
try {
|
|
11773
|
+
const results = await Promise.all(searchSources.map(async (source) => {
|
|
11774
|
+
const controller = new AbortController();
|
|
11775
|
+
abortControllersRef.current.set(source.id, controller);
|
|
11776
|
+
const items = await fetchFromSource(source, query, controller.signal);
|
|
11777
|
+
return { source, items };
|
|
11778
|
+
}));
|
|
11779
|
+
const groupedResults = results.filter(r => r.items.length > 0);
|
|
11780
|
+
dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: groupedResults });
|
|
11781
|
+
}
|
|
11782
|
+
catch (error) {
|
|
11783
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
11784
|
+
return;
|
|
11785
|
+
dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Search failed' });
|
|
11786
|
+
}
|
|
11787
|
+
finally {
|
|
11788
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
11789
|
+
}
|
|
11790
|
+
}, [searchSources, fetchFromSource]);
|
|
11791
|
+
const search = React.useCallback((q) => fetchSuggestions(q), [fetchSuggestions]);
|
|
11792
|
+
const setQuery = React.useCallback((query) => {
|
|
11793
|
+
dispatch({ type: 'SET_QUERY', payload: query });
|
|
11794
|
+
if (debounceTimerRef.current)
|
|
11795
|
+
clearTimeout(debounceTimerRef.current);
|
|
11796
|
+
debounceTimerRef.current = setTimeout(() => fetchSuggestions(query), debounceMs);
|
|
11797
|
+
}, [fetchSuggestions, debounceMs]);
|
|
11798
|
+
const selectNext = React.useCallback(() => dispatch({ type: 'SELECT_NEXT' }), []);
|
|
11799
|
+
const selectPrev = React.useCallback(() => dispatch({ type: 'SELECT_PREV' }), []);
|
|
11800
|
+
const setSelectedIndex = React.useCallback((index) => dispatch({ type: 'SET_SELECTED_INDEX', payload: index }), []);
|
|
11801
|
+
const reset = React.useCallback(() => {
|
|
11802
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11803
|
+
abortControllersRef.current.clear();
|
|
11804
|
+
if (debounceTimerRef.current)
|
|
11805
|
+
clearTimeout(debounceTimerRef.current);
|
|
11806
|
+
dispatch({ type: 'RESET' });
|
|
11807
|
+
}, []);
|
|
11808
|
+
const getSelectedItem = React.useCallback(() => {
|
|
11809
|
+
const items = state.mode === 'results' ? state.results : state.suggestions;
|
|
11810
|
+
return items[state.selectedIndex] || null;
|
|
11811
|
+
}, [state.mode, state.results, state.suggestions, state.selectedIndex]);
|
|
11812
|
+
React.useEffect(() => {
|
|
11813
|
+
return () => {
|
|
11814
|
+
abortControllersRef.current.forEach(c => c.abort());
|
|
11815
|
+
abortControllersRef.current.clear();
|
|
11816
|
+
if (debounceTimerRef.current)
|
|
11817
|
+
clearTimeout(debounceTimerRef.current);
|
|
11818
|
+
};
|
|
11819
|
+
}, []);
|
|
11820
|
+
return {
|
|
11821
|
+
...state,
|
|
11822
|
+
sources: searchSources,
|
|
11823
|
+
setQuery,
|
|
11824
|
+
search,
|
|
11825
|
+
fetchSuggestions,
|
|
11826
|
+
selectNext,
|
|
11827
|
+
selectPrev,
|
|
11828
|
+
setSelectedIndex,
|
|
11829
|
+
reset,
|
|
11830
|
+
getSelectedItem,
|
|
11831
|
+
};
|
|
11832
|
+
}
|
|
11833
|
+
|
|
11834
|
+
function transformResults(results) {
|
|
11835
|
+
return results.map((result) => {
|
|
11836
|
+
const url = result.url || result.route || result.link || '';
|
|
11837
|
+
const title = result.title || result.name || '';
|
|
11838
|
+
const content = result.content || result.description || result.snippet || '';
|
|
11839
|
+
const description = result.description || result.content?.substring?.(0, 150) || '';
|
|
11840
|
+
const hierarchy = {};
|
|
11841
|
+
if (result.hierarchy) {
|
|
11842
|
+
hierarchy.lvl0 = result.hierarchy.lvl0;
|
|
11843
|
+
hierarchy.lvl1 = result.hierarchy.lvl1;
|
|
11844
|
+
hierarchy.lvl2 = result.hierarchy.lvl2;
|
|
11845
|
+
}
|
|
11846
|
+
else {
|
|
11847
|
+
if (result.category)
|
|
11848
|
+
hierarchy.lvl0 = result.category;
|
|
11849
|
+
if (result.parent_title || result.parentTitle)
|
|
11850
|
+
hierarchy.lvl1 = result.parent_title || result.parentTitle;
|
|
11851
|
+
}
|
|
11852
|
+
return {
|
|
11853
|
+
url,
|
|
11854
|
+
title: title?.replace?.(/<\/?mark>/g, '') || title,
|
|
11855
|
+
content: content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 200) || content,
|
|
11856
|
+
description: description?.replace?.(/<\/?mark>/g, '') || description,
|
|
11857
|
+
category: result.category || hierarchy.lvl0 || '',
|
|
11858
|
+
hierarchy,
|
|
11859
|
+
route: result.route,
|
|
11860
|
+
parentTitle: result.parent_title || result.parentTitle,
|
|
11861
|
+
type: result.type || '',
|
|
11862
|
+
anchor: result.anchor || '',
|
|
11863
|
+
_source: 'seekora',
|
|
11864
|
+
};
|
|
11865
|
+
});
|
|
11866
|
+
}
|
|
11867
|
+
function useSeekoraSearch$1(options) {
|
|
11868
|
+
const { storeId, storeSecret, apiEndpoint, maxResults = 20, debounceMs = 200, analyticsTags = ['docsearch'], groupField, groupSize, } = options;
|
|
11869
|
+
const [query, setQueryState] = React.useState('');
|
|
11870
|
+
const [suggestions, setSuggestions] = React.useState([]);
|
|
11871
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
11872
|
+
const [error, setError] = React.useState(null);
|
|
11873
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
11874
|
+
const clientRef = React.useRef(null);
|
|
11875
|
+
const lastSearchContextRef = React.useRef(null);
|
|
11876
|
+
const debounceTimerRef = React.useRef(null);
|
|
11877
|
+
const abortControllerRef = React.useRef(null);
|
|
11878
|
+
React.useEffect(() => {
|
|
11879
|
+
if (!storeId)
|
|
11880
|
+
return;
|
|
11881
|
+
const config = {
|
|
11882
|
+
storeId,
|
|
11883
|
+
readSecret: storeSecret,
|
|
11884
|
+
logLevel: 'warn',
|
|
11885
|
+
enableContextCollection: true,
|
|
11886
|
+
};
|
|
11887
|
+
if (apiEndpoint) {
|
|
11888
|
+
if (['local', 'stage', 'production'].includes(apiEndpoint)) {
|
|
11889
|
+
config.environment = apiEndpoint;
|
|
11890
|
+
}
|
|
11891
|
+
else {
|
|
11892
|
+
config.baseUrl = apiEndpoint;
|
|
11893
|
+
}
|
|
11894
|
+
}
|
|
11895
|
+
try {
|
|
11896
|
+
clientRef.current = new searchSdk.SeekoraClient(config);
|
|
11897
|
+
}
|
|
11898
|
+
catch (err) {
|
|
11899
|
+
console.error('Failed to initialize SeekoraClient:', err);
|
|
11900
|
+
setError('Failed to initialize search client');
|
|
11901
|
+
}
|
|
11902
|
+
return () => {
|
|
11903
|
+
clientRef.current = null;
|
|
11904
|
+
};
|
|
11905
|
+
}, [storeId, storeSecret, apiEndpoint]);
|
|
11906
|
+
const performSearch = React.useCallback(async (searchQuery) => {
|
|
11907
|
+
if (!clientRef.current) {
|
|
11908
|
+
setError('Search client not initialized');
|
|
11909
|
+
return;
|
|
11910
|
+
}
|
|
11911
|
+
if (!searchQuery.trim()) {
|
|
11912
|
+
setSuggestions([]);
|
|
11913
|
+
return;
|
|
11914
|
+
}
|
|
11915
|
+
if (abortControllerRef.current)
|
|
11916
|
+
abortControllerRef.current.abort();
|
|
11917
|
+
abortControllerRef.current = new AbortController();
|
|
11918
|
+
setIsLoading(true);
|
|
11919
|
+
setError(null);
|
|
11920
|
+
try {
|
|
11921
|
+
const response = await clientRef.current.search(searchQuery, {
|
|
11922
|
+
per_page: maxResults,
|
|
11923
|
+
analytics_tags: analyticsTags,
|
|
11924
|
+
return_fields: [
|
|
11925
|
+
'hierarchy.lvl0', 'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
|
|
11926
|
+
'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6',
|
|
11927
|
+
'content', 'type', 'url', 'title', 'anchor'
|
|
11928
|
+
],
|
|
11929
|
+
snippet_fields: [
|
|
11930
|
+
'hierarchy.lvl1', 'hierarchy.lvl2', 'hierarchy.lvl3',
|
|
11931
|
+
'hierarchy.lvl4', 'hierarchy.lvl5', 'hierarchy.lvl6', 'content'
|
|
11932
|
+
],
|
|
11933
|
+
snippet_prefix: '<mark>',
|
|
11934
|
+
snippet_suffix: '</mark>',
|
|
11935
|
+
include_snippets: true,
|
|
11936
|
+
group_field: groupField,
|
|
11937
|
+
group_size: groupSize,
|
|
11938
|
+
});
|
|
11939
|
+
if (abortControllerRef.current?.signal.aborted)
|
|
11940
|
+
return;
|
|
11941
|
+
if (response?.context)
|
|
11942
|
+
lastSearchContextRef.current = response.context;
|
|
11943
|
+
setSuggestions(transformResults(response.results || []));
|
|
11944
|
+
setSelectedIndex(0);
|
|
11945
|
+
}
|
|
11946
|
+
catch (err) {
|
|
11947
|
+
if (err instanceof Error && err.name === 'AbortError')
|
|
11948
|
+
return;
|
|
11949
|
+
console.error('Search failed:', err);
|
|
11950
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
11951
|
+
setSuggestions([]);
|
|
11952
|
+
}
|
|
11953
|
+
finally {
|
|
11954
|
+
setIsLoading(false);
|
|
11955
|
+
}
|
|
11956
|
+
}, [maxResults, analyticsTags, groupField, groupSize]);
|
|
11957
|
+
const setQuery = React.useCallback((newQuery) => {
|
|
11958
|
+
setQueryState(newQuery);
|
|
11959
|
+
setSelectedIndex(0);
|
|
11960
|
+
if (debounceTimerRef.current)
|
|
11961
|
+
clearTimeout(debounceTimerRef.current);
|
|
11962
|
+
debounceTimerRef.current = setTimeout(() => performSearch(newQuery), debounceMs);
|
|
11963
|
+
}, [performSearch, debounceMs]);
|
|
11964
|
+
const selectNext = React.useCallback(() => {
|
|
11965
|
+
setSelectedIndex((prev) => (prev >= suggestions.length - 1 ? 0 : prev + 1));
|
|
11966
|
+
}, [suggestions.length]);
|
|
11967
|
+
const selectPrev = React.useCallback(() => {
|
|
11968
|
+
setSelectedIndex((prev) => (prev <= 0 ? suggestions.length - 1 : prev - 1));
|
|
11969
|
+
}, [suggestions.length]);
|
|
11970
|
+
const reset = React.useCallback(() => {
|
|
11971
|
+
if (abortControllerRef.current)
|
|
11972
|
+
abortControllerRef.current.abort();
|
|
11973
|
+
if (debounceTimerRef.current)
|
|
11974
|
+
clearTimeout(debounceTimerRef.current);
|
|
11975
|
+
setQueryState('');
|
|
11976
|
+
setSuggestions([]);
|
|
11977
|
+
setIsLoading(false);
|
|
11978
|
+
setError(null);
|
|
11979
|
+
setSelectedIndex(0);
|
|
11980
|
+
}, []);
|
|
11981
|
+
const getSelectedItem = React.useCallback(() => {
|
|
11982
|
+
return suggestions[selectedIndex] || null;
|
|
11983
|
+
}, [suggestions, selectedIndex]);
|
|
11984
|
+
const trackDocClick = React.useCallback((hit, position) => {
|
|
11985
|
+
const client = clientRef.current;
|
|
11986
|
+
if (!client?.trackEvent)
|
|
11987
|
+
return;
|
|
11988
|
+
const context = lastSearchContextRef.current ?? undefined;
|
|
11989
|
+
const itemId = hit.url || hit.id || hit.title || String(position);
|
|
11990
|
+
client.trackEvent({
|
|
11991
|
+
event_name: 'doc_click',
|
|
11992
|
+
clicked_item_id: itemId,
|
|
11993
|
+
metadata: { position, result: hit, source: 'docsearch' },
|
|
11994
|
+
}, context);
|
|
11995
|
+
}, []);
|
|
11996
|
+
React.useEffect(() => {
|
|
11997
|
+
return () => {
|
|
11998
|
+
if (abortControllerRef.current)
|
|
11999
|
+
abortControllerRef.current.abort();
|
|
12000
|
+
if (debounceTimerRef.current)
|
|
12001
|
+
clearTimeout(debounceTimerRef.current);
|
|
12002
|
+
};
|
|
12003
|
+
}, []);
|
|
12004
|
+
return {
|
|
12005
|
+
query,
|
|
12006
|
+
suggestions,
|
|
12007
|
+
isLoading,
|
|
12008
|
+
error,
|
|
12009
|
+
selectedIndex,
|
|
12010
|
+
setQuery,
|
|
12011
|
+
selectNext,
|
|
12012
|
+
selectPrev,
|
|
12013
|
+
setSelectedIndex,
|
|
12014
|
+
reset,
|
|
12015
|
+
getSelectedItem,
|
|
12016
|
+
trackDocClick,
|
|
12017
|
+
};
|
|
12018
|
+
}
|
|
12019
|
+
|
|
12020
|
+
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', }) {
|
|
12021
|
+
const [isOpen, setIsOpen] = React.useState(initialOpen);
|
|
12022
|
+
const useSeekoraSDK = !!storeId;
|
|
12023
|
+
const seekoraSearch = useSeekoraSearch$1({
|
|
12024
|
+
storeId: storeId || '',
|
|
12025
|
+
storeSecret,
|
|
12026
|
+
apiEndpoint: seekoraApiEndpoint,
|
|
12027
|
+
maxResults,
|
|
12028
|
+
debounceMs,
|
|
12029
|
+
analyticsTags: ['docsearch'],
|
|
12030
|
+
});
|
|
12031
|
+
const legacySearch = useDocSearch({
|
|
12032
|
+
apiEndpoint,
|
|
12033
|
+
apiKey,
|
|
12034
|
+
sources,
|
|
12035
|
+
maxResults,
|
|
12036
|
+
debounceMs,
|
|
12037
|
+
});
|
|
12038
|
+
const { query, suggestions, isLoading, error, selectedIndex, setQuery, selectNext, selectPrev, setSelectedIndex, reset, getSelectedItem, } = useSeekoraSDK ? seekoraSearch : legacySearch;
|
|
12039
|
+
const groupedSuggestions = useSeekoraSDK ? undefined : legacySearch.groupedSuggestions;
|
|
12040
|
+
const results = useSeekoraSDK ? suggestions : legacySearch.results;
|
|
12041
|
+
const mode = useSeekoraSDK ? 'suggestions' : legacySearch.mode;
|
|
12042
|
+
const searchSources = useSeekoraSDK
|
|
12043
|
+
? [{ id: 'seekora', name: 'Results', endpoint: '' }]
|
|
12044
|
+
: legacySearch.sources;
|
|
12045
|
+
const handleOpen = React.useCallback(() => setIsOpen(true), []);
|
|
12046
|
+
const handleClose = React.useCallback(() => {
|
|
12047
|
+
setIsOpen(false);
|
|
12048
|
+
reset();
|
|
12049
|
+
onClose?.();
|
|
12050
|
+
}, [reset, onClose]);
|
|
12051
|
+
const handleSelect = React.useCallback((hit) => {
|
|
12052
|
+
if (useSeekoraSDK && seekoraSearch.trackDocClick) {
|
|
12053
|
+
seekoraSearch.trackDocClick(hit, selectedIndex + 1);
|
|
12054
|
+
}
|
|
12055
|
+
if (onSelect) {
|
|
12056
|
+
onSelect(hit);
|
|
12057
|
+
}
|
|
12058
|
+
else {
|
|
12059
|
+
window.location.href = hit.url;
|
|
12060
|
+
}
|
|
12061
|
+
handleClose();
|
|
12062
|
+
}, [onSelect, handleClose, useSeekoraSDK, seekoraSearch, selectedIndex]);
|
|
12063
|
+
const handleEnter = React.useCallback(() => {
|
|
12064
|
+
const selectedItem = getSelectedItem();
|
|
12065
|
+
if (selectedItem)
|
|
12066
|
+
handleSelect(selectedItem);
|
|
12067
|
+
}, [getSelectedItem, handleSelect]);
|
|
12068
|
+
const { handleModalKeyDown } = useKeyboard({
|
|
12069
|
+
isOpen,
|
|
12070
|
+
onOpen: handleOpen,
|
|
12071
|
+
onClose: handleClose,
|
|
12072
|
+
onSelectNext: selectNext,
|
|
12073
|
+
onSelectPrev: selectPrev,
|
|
12074
|
+
onEnter: handleEnter,
|
|
12075
|
+
disableShortcut,
|
|
12076
|
+
shortcutKey,
|
|
12077
|
+
});
|
|
12078
|
+
const handleKeyDown = React.useCallback((event) => handleModalKeyDown(event), [handleModalKeyDown]);
|
|
12079
|
+
const displayHits = mode === 'results' ? results : suggestions;
|
|
12080
|
+
return (React.createElement(React.Fragment, null,
|
|
12081
|
+
renderButton && (React.createElement(ButtonComponent, { onClick: handleOpen, placeholder: translations.buttonText || placeholder })),
|
|
12082
|
+
React.createElement(Modal, { isOpen: isOpen, onClose: handleClose },
|
|
12083
|
+
React.createElement("div", { className: "seekora-docsearch-modal", onKeyDown: handleKeyDown },
|
|
12084
|
+
React.createElement("header", { className: "seekora-docsearch-header" },
|
|
12085
|
+
React.createElement(SearchBox, { value: query, onChange: setQuery, onKeyDown: handleKeyDown, placeholder: placeholder, isLoading: isLoading, onClear: reset }),
|
|
12086
|
+
React.createElement("button", { type: "button", className: "seekora-docsearch-close", onClick: handleClose, "aria-label": "Close search" },
|
|
12087
|
+
React.createElement("span", { className: "seekora-docsearch-close-text" }, "esc"))),
|
|
12088
|
+
React.createElement("div", { className: "seekora-docsearch-body" },
|
|
12089
|
+
React.createElement(Results, { hits: displayHits, groupedHits: groupedSuggestions, selectedIndex: selectedIndex, onSelect: handleSelect, onHover: setSelectedIndex, query: query, isLoading: isLoading, error: error, translations: translations, sources: searchSources })),
|
|
12090
|
+
React.createElement(Footer, { translations: translations })))));
|
|
12091
|
+
}
|
|
12092
|
+
|
|
10566
12093
|
/**
|
|
10567
12094
|
* useSeekoraSearch Hook
|
|
10568
12095
|
*
|
|
@@ -10628,7 +12155,9 @@ const useSeekoraSearch = ({ client, autoTrack = true, }) => {
|
|
|
10628
12155
|
/**
|
|
10629
12156
|
* useAnalytics Hook
|
|
10630
12157
|
*
|
|
10631
|
-
* Hook for tracking analytics events with the Seekora SDK
|
|
12158
|
+
* Hook for tracking analytics events with the Seekora SDK.
|
|
12159
|
+
* Supports Analytics V3 payload fields (event_ts, anonymous_id, orgcode, xstoreid);
|
|
12160
|
+
* the SDK sends both legacy and v3 fields for backend compatibility.
|
|
10632
12161
|
*/
|
|
10633
12162
|
const useAnalytics = ({ client, enabled = true, }) => {
|
|
10634
12163
|
const trackEvent = React.useCallback(async (eventType, payload, context) => {
|
|
@@ -10648,14 +12177,17 @@ const useAnalytics = ({ client, enabled = true, }) => {
|
|
|
10648
12177
|
const trackClick = React.useCallback(async (resultId, result, context, position) => {
|
|
10649
12178
|
if (!enabled)
|
|
10650
12179
|
return;
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
12180
|
+
const pos = position ?? 0;
|
|
12181
|
+
if (client.trackClick) {
|
|
12182
|
+
await client.trackClick(resultId, pos, context);
|
|
12183
|
+
}
|
|
12184
|
+
else {
|
|
12185
|
+
await trackEvent('product_click', {
|
|
12186
|
+
clicked_item_id: resultId,
|
|
12187
|
+
metadata: { result, ...(position !== undefined && { position: pos }) },
|
|
12188
|
+
}, context);
|
|
12189
|
+
}
|
|
12190
|
+
}, [client, trackEvent, enabled]);
|
|
10659
12191
|
const trackConversion = React.useCallback(async (resultId, result, value, currency, context) => {
|
|
10660
12192
|
if (!enabled)
|
|
10661
12193
|
return;
|
|
@@ -11681,12 +13213,15 @@ exports.AmazonDropdown = AmazonDropdown;
|
|
|
11681
13213
|
exports.Breadcrumb = Breadcrumb;
|
|
11682
13214
|
exports.ClearRefinements = ClearRefinements;
|
|
11683
13215
|
exports.CurrentRefinements = CurrentRefinements;
|
|
13216
|
+
exports.DocSearch = DocSearch;
|
|
13217
|
+
exports.DocSearchButton = DocSearchButton;
|
|
11684
13218
|
exports.Facets = Facets;
|
|
11685
13219
|
exports.FederatedDropdown = FederatedDropdown;
|
|
13220
|
+
exports.Fingerprint = Fingerprint;
|
|
11686
13221
|
exports.FrequentlyBoughtTogether = FrequentlyBoughtTogether;
|
|
11687
13222
|
exports.GoogleDropdown = GoogleDropdown;
|
|
11688
13223
|
exports.HierarchicalMenu = HierarchicalMenu;
|
|
11689
|
-
exports.Highlight = Highlight;
|
|
13224
|
+
exports.Highlight = Highlight$1;
|
|
11690
13225
|
exports.HitsPerPage = HitsPerPage;
|
|
11691
13226
|
exports.InfiniteHits = InfiniteHits;
|
|
11692
13227
|
exports.MinimalDropdown = MinimalDropdown;
|
|
@@ -11734,7 +13269,9 @@ exports.extractSuggestion = extractSuggestion;
|
|
|
11734
13269
|
exports.formatParsedFilters = formatParsedFilters;
|
|
11735
13270
|
exports.formatSuggestionPrice = formatPrice;
|
|
11736
13271
|
exports.generateSuggestionsStylesheet = generateSuggestionsStylesheet;
|
|
13272
|
+
exports.getFingerprint = getFingerprint;
|
|
11737
13273
|
exports.getRecentSearches = getRecentSearches;
|
|
13274
|
+
exports.getShortcutText = getShortcutText;
|
|
11738
13275
|
exports.getSuggestionsCache = getSuggestionsCache;
|
|
11739
13276
|
exports.highlightText = highlightText;
|
|
11740
13277
|
exports.injectGlobalResponsiveStyles = injectGlobalResponsiveStyles;
|
|
@@ -11748,7 +13285,10 @@ exports.removeRecentSearch = removeRecentSearch;
|
|
|
11748
13285
|
exports.touchTargets = touchTargets;
|
|
11749
13286
|
exports.updateSuggestionsStyles = updateSuggestionsStyles;
|
|
11750
13287
|
exports.useAnalytics = useAnalytics;
|
|
13288
|
+
exports.useDocSearch = useDocSearch;
|
|
13289
|
+
exports.useDocSearchSeekoraSearch = useSeekoraSearch$1;
|
|
11751
13290
|
exports.useInjectResponsiveStyles = useInjectResponsiveStyles;
|
|
13291
|
+
exports.useKeyboard = useKeyboard;
|
|
11752
13292
|
exports.useNaturalLanguageFilters = useNaturalLanguageFilters;
|
|
11753
13293
|
exports.useQuerySuggestions = useQuerySuggestions;
|
|
11754
13294
|
exports.useQuerySuggestionsEnhanced = useQuerySuggestionsEnhanced;
|