@blotoutio/providers-evo-search-sdk 1.66.1 → 1.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/core.cjs.js +230 -4
- package/core.js +230 -4
- package/core.mjs +230 -4
- package/hooks.cjs.js +320 -3
- package/hooks.d.ts +58 -1
- package/hooks.mjs +318 -4
- package/index.cjs.js +2192 -82
- package/index.js +2192 -82
- package/index.mjs +2192 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,122 @@ function HeaderSearch() {
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Image search
|
|
58
|
+
|
|
59
|
+
Shoppers can search with a photo instead of words. The hook exposes it as one extra action that writes the same `results` / `loading` / `error` state as the text path, so you render one result list:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { useEvoSearch } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
63
|
+
|
|
64
|
+
function ImageSearch() {
|
|
65
|
+
const { query, searchByImage, results, loading, error } = useEvoSearch()
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<input
|
|
69
|
+
type='file'
|
|
70
|
+
accept='image/*'
|
|
71
|
+
onChange={(e) => {
|
|
72
|
+
const file = e.target.files?.[0]
|
|
73
|
+
if (file) {
|
|
74
|
+
// Second argument is an optional text refinement ("in blue").
|
|
75
|
+
void searchByImage(file, query)
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
{loading && <span>Searching…</span>}
|
|
80
|
+
{error && <span>Image search failed. Try again.</span>}
|
|
81
|
+
<ul>
|
|
82
|
+
{results.map((p) => (
|
|
83
|
+
<li key={p.id}>{p.title}</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pass a `File` or `Blob` straight from the file input (or a drop / paste handler) — it is downscaled and JPEG-encoded for you. A `string` is taken as an already-prepared `data:` URI and sent as-is.
|
|
92
|
+
|
|
93
|
+
### Check the merchant gate first
|
|
94
|
+
|
|
95
|
+
Image search is off unless the merchant enabled it, exactly like Ask AI. Read it from the API config and hide the affordance when it is not enabled:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { useEffect, useState } from 'react'
|
|
99
|
+
import { useEvoSearchAPI } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
100
|
+
|
|
101
|
+
function useVisualSearchEnabled() {
|
|
102
|
+
const { api, ready } = useEvoSearchAPI()
|
|
103
|
+
const [enabled, setEnabled] = useState(false)
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!ready || !api) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
void api
|
|
109
|
+
// Older workers omit the field, so compare strictly — a missing value
|
|
110
|
+
// means off, never on.
|
|
111
|
+
.config()
|
|
112
|
+
.then((config) => setEnabled(config.visualSearchEnabled === true))
|
|
113
|
+
.catch(() => setEnabled(false))
|
|
114
|
+
}, [api, ready])
|
|
115
|
+
return enabled
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If the merchant turns it off between that check and a search, the request fails with `403` and the rejection carries `disabled: true`. Treat that as "hide the entry point", not as an error to show:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import type { EvoSearchAPIError } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
123
|
+
|
|
124
|
+
if ((error as EvoSearchAPIError | null)?.disabled) {
|
|
125
|
+
// Hide the camera button — nothing the shopper can retry.
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### The raw API (non-React, or your own state)
|
|
130
|
+
|
|
131
|
+
In React, `useEvoSearchAPI()` hands you the API object:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
const { api } = useEvoSearchAPI()
|
|
135
|
+
const { mode, products } = await api.searchImage(file, 'in blue')
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
It is the same object the registry holds, so a vanilla storefront can reach it without the hook:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const api = window[Symbol.for('evo-search')]?.api
|
|
142
|
+
const { mode, products } = await api.searchImage(file, 'in blue')
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`searchImage` returns the API product shape, not the normalized one `useEvoSearch().results` yields. Map it if you want both paths to render through the same components:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { mapAPIProductToSDKProduct } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
149
|
+
|
|
150
|
+
const normalized = products.map(mapAPIProductToSDKProduct)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Timeouts are your responsibility on the raw API
|
|
154
|
+
|
|
155
|
+
`searchImage` has no built-in deadline: pass an `AbortSignal` so a stalled embedding can't hang your UI forever. Do not retry — every attempt costs a multimodal embedding, and there is no response cache on this route. `useEvoSearch().searchByImage` already applies this policy (no retries, 15s budget):
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const controller = new AbortController()
|
|
159
|
+
const timer = setTimeout(() => controller.abort(), 15_000)
|
|
160
|
+
try {
|
|
161
|
+
await api.searchImage(file, undefined, undefined, controller.signal)
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timer)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Debounce and abort rather than firing per keystroke when a text refinement is attached to an image.
|
|
168
|
+
|
|
169
|
+
### Why 768px
|
|
170
|
+
|
|
171
|
+
Uploads are downscaled to a 768px long edge before they leave the browser: retrieval quality was tuned at that rendition, and the endpoint caps the decoded payload (~1.5MB), which a phone photo blows past. If you want to prepare or preview the data URI yourself, `downscaleImage(file)` and `MAX_IMAGE_EDGE` are exported from the same entry point — it rejects anything that isn't an image.
|
|
172
|
+
|
|
57
173
|
## Tracking interactions
|
|
58
174
|
|
|
59
175
|
EvoSearch needs three interaction signals to power the dashboard: searches, result clicks, and cart/purchase events. **In a headless integration, only `trackClick` requires code on your side.**
|
package/core.cjs.js
CHANGED
|
@@ -598,6 +598,19 @@ const hasPreviewKey = () => {
|
|
|
598
598
|
return false;
|
|
599
599
|
}
|
|
600
600
|
};
|
|
601
|
+
// Session-sticky opt-out for internal QA: ?evs=off suppresses the widget even
|
|
602
|
+
// when the manifest has it enabled, so the merchant's default site can be
|
|
603
|
+
// previewed. Takes precedence over both `enabled` and the preview flag.
|
|
604
|
+
const disableKeyName = 'disableEvoSearch';
|
|
605
|
+
const hasDisableKey = () => {
|
|
606
|
+
var _a;
|
|
607
|
+
try {
|
|
608
|
+
return ((_a = sessionStorage.getItem(disableKeyName)) !== null && _a !== void 0 ? _a : '0') === '1';
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
601
614
|
|
|
602
615
|
var _a;
|
|
603
616
|
const registryKey = Symbol.for('evo-search');
|
|
@@ -640,6 +653,10 @@ const notifyReady = () => {
|
|
|
640
653
|
// caller-superseded (aborted) request. Shared by the widget and the headless
|
|
641
654
|
// hook. See callers: search-widget.ts (performAutocomplete) and
|
|
642
655
|
// hooks/use-evo-search.ts.
|
|
656
|
+
// Per-attempt budget for /search/image, shared by the widget and the headless
|
|
657
|
+
// hook so the two can't drift. Wider than the text search's 7s: the request
|
|
658
|
+
// carries an upload and waits on a multimodal embedding.
|
|
659
|
+
const IMAGE_SEARCH_TIMEOUT_MS = 15000;
|
|
643
660
|
const DEFAULTS = {
|
|
644
661
|
timeoutMs: 7000,
|
|
645
662
|
retries: 2,
|
|
@@ -743,6 +760,126 @@ async function withSearchResilience(attempt, callerSignal, options = {}) {
|
|
|
743
760
|
throw lastError;
|
|
744
761
|
}
|
|
745
762
|
|
|
763
|
+
// Client-side downscale for image search. The worker caps the decoded upload at
|
|
764
|
+
// ~1.5MB and every request costs a multimodal embed, so shrinking a phone photo
|
|
765
|
+
// here (a 12MP JPEG is ~4MB) is what keeps the request inside the cap and the
|
|
766
|
+
// upload quick on mobile data. Matches the sync-time `imageEmbedWidth` (see
|
|
767
|
+
// search-tuning.ts) so query and index renditions land in the same Gemini
|
|
768
|
+
// tiling bracket — a JPEG at this edge is still two orders of magnitude below
|
|
769
|
+
// the worker's decoded-size cap.
|
|
770
|
+
const MAX_IMAGE_EDGE = 768;
|
|
771
|
+
const JPEG_QUALITY = 0.85;
|
|
772
|
+
// Fallback decode for engines without createImageBitmap. Orientation here is
|
|
773
|
+
// whatever the UA's `image-orientation` default is — see decodeImage.
|
|
774
|
+
const loadImage = (source) => new Promise((resolve, reject) => {
|
|
775
|
+
const objectUrl = URL.createObjectURL(source);
|
|
776
|
+
const image = new Image();
|
|
777
|
+
image.onload = () => {
|
|
778
|
+
URL.revokeObjectURL(objectUrl);
|
|
779
|
+
resolve(image);
|
|
780
|
+
};
|
|
781
|
+
image.onerror = () => {
|
|
782
|
+
URL.revokeObjectURL(objectUrl);
|
|
783
|
+
reject(new Error('Image could not be decoded'));
|
|
784
|
+
};
|
|
785
|
+
image.src = objectUrl;
|
|
786
|
+
});
|
|
787
|
+
// createImageBitmap pins EXIF orientation explicitly, so a portrait phone photo
|
|
788
|
+
// is embedded upright even on engines that predate the
|
|
789
|
+
// `image-orientation: from-image` default (older iOS Safari, the embedded
|
|
790
|
+
// Android WebViews storefront traffic includes) — a sideways embed degrades
|
|
791
|
+
// retrieval with no error to show for it. It also skips the full-size <img>
|
|
792
|
+
// decode. So it goes first, and the <img> path catches everything it can't do:
|
|
793
|
+
// engines that lack the API, and the sources it rejects outright but <img>
|
|
794
|
+
// renders (an SVG with no intrinsic size is the one that shows up).
|
|
795
|
+
// A very large source can still yield a blank canvas on old iOS; if blank
|
|
796
|
+
// embeds are ever observed, add a source pixel-count ceiling here.
|
|
797
|
+
const decodeImage = async (source) => {
|
|
798
|
+
if (typeof createImageBitmap === 'function') {
|
|
799
|
+
try {
|
|
800
|
+
const bitmap = await createImageBitmap(source, {
|
|
801
|
+
imageOrientation: 'from-image',
|
|
802
|
+
});
|
|
803
|
+
return {
|
|
804
|
+
image: bitmap,
|
|
805
|
+
width: bitmap.width,
|
|
806
|
+
height: bitmap.height,
|
|
807
|
+
// Phone memory doesn't wait for GC to reclaim a decoded bitmap.
|
|
808
|
+
release: () => bitmap.close(),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
// Fall through: a rejection here means "this decoder can't", not "this
|
|
813
|
+
// isn't an image". The legacy path decides, and only its failure is the
|
|
814
|
+
// shopper's error.
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const image = await loadImage(source);
|
|
818
|
+
return {
|
|
819
|
+
image,
|
|
820
|
+
width: image.naturalWidth,
|
|
821
|
+
height: image.naturalHeight,
|
|
822
|
+
release: () => undefined,
|
|
823
|
+
};
|
|
824
|
+
};
|
|
825
|
+
// Draw `image` — or just `region` of it — into a fresh canvas whose longest
|
|
826
|
+
// edge is at most `maxEdge`, on a white ground. A cropped region is scaled to
|
|
827
|
+
// the same edge cap as the whole frame, so a selection is embedded at the
|
|
828
|
+
// resolution the index renditions were built at rather than at whatever
|
|
829
|
+
// fraction of the frame it happened to occupy.
|
|
830
|
+
const renderToCanvas = (image, sourceWidth, sourceHeight, maxEdge, region) => {
|
|
831
|
+
const sx = region ? Math.round(region.x * sourceWidth) : 0;
|
|
832
|
+
const sy = region ? Math.round(region.y * sourceHeight) : 0;
|
|
833
|
+
const sw = region
|
|
834
|
+
? Math.max(1, Math.round(region.w * sourceWidth))
|
|
835
|
+
: sourceWidth;
|
|
836
|
+
const sh = region
|
|
837
|
+
? Math.max(1, Math.round(region.h * sourceHeight))
|
|
838
|
+
: sourceHeight;
|
|
839
|
+
const longestEdge = Math.max(sw, sh);
|
|
840
|
+
const scale = longestEdge > maxEdge ? maxEdge / longestEdge : 1;
|
|
841
|
+
const canvas = document.createElement('canvas');
|
|
842
|
+
canvas.width = Math.max(1, Math.round(sw * scale));
|
|
843
|
+
canvas.height = Math.max(1, Math.round(sh * scale));
|
|
844
|
+
const context = canvas.getContext('2d');
|
|
845
|
+
if (!context) {
|
|
846
|
+
throw new Error('Canvas is unavailable');
|
|
847
|
+
}
|
|
848
|
+
// JPEG has no alpha, so a transparent source (a PNG/WebP cutout, the common
|
|
849
|
+
// paste case) would otherwise composite onto the canvas's transparent-black
|
|
850
|
+
// default and be embedded as a black-backed product. Catalog shots are
|
|
851
|
+
// typically white-background, so white is the closer prior.
|
|
852
|
+
context.fillStyle = '#ffffff';
|
|
853
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
854
|
+
if (region) {
|
|
855
|
+
context.drawImage(image, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
859
|
+
}
|
|
860
|
+
return canvas;
|
|
861
|
+
};
|
|
862
|
+
/**
|
|
863
|
+
* Downscale `source` to a JPEG data URI whose longest edge is at most
|
|
864
|
+
* `MAX_IMAGE_EDGE`, upright regardless of the source's EXIF orientation.
|
|
865
|
+
* Rejects anything that isn't an image, and anything the browser can't decode
|
|
866
|
+
* (HEIC off iOS, corrupt files) — every input path (file picker, drop, paste) is
|
|
867
|
+
* shopper-controlled, so this is the trust boundary that keeps a stray PDF or
|
|
868
|
+
* text drop off the wire.
|
|
869
|
+
*/
|
|
870
|
+
const downscaleImage = async (source) => {
|
|
871
|
+
if (!source.type.startsWith('image/')) {
|
|
872
|
+
throw new Error('Only image files can be searched');
|
|
873
|
+
}
|
|
874
|
+
const { image, width, height, release } = await decodeImage(source);
|
|
875
|
+
try {
|
|
876
|
+
return renderToCanvas(image, width, height, MAX_IMAGE_EDGE).toDataURL('image/jpeg', JPEG_QUALITY);
|
|
877
|
+
}
|
|
878
|
+
finally {
|
|
879
|
+
release();
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
746
883
|
// Ask can't reuse withSearchResilience: a half-consumed stream isn't retryable.
|
|
747
884
|
// Idle timeout resets per chunk; both reject with TimeoutError so submitAsk
|
|
748
885
|
// routes them to the search fallback rather than treating them as cancellation.
|
|
@@ -926,6 +1063,62 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
926
1063
|
logger.info(`EvoSearch API: Search success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
|
|
927
1064
|
return data;
|
|
928
1065
|
};
|
|
1066
|
+
// Image search. Unlike `/search` there is no response cache on the worker
|
|
1067
|
+
// side and every call costs a multimodal embedding, so callers must debounce
|
|
1068
|
+
// and abort rather than fire per keystroke. A 403 `{disabled:true}` means the
|
|
1069
|
+
// merchant turned visual search off — surfaced as `disabled` on the thrown
|
|
1070
|
+
// error so the widget can hide the affordance instead of showing an error.
|
|
1071
|
+
//
|
|
1072
|
+
// A File/Blob (the common case headlessly: straight off an `<input
|
|
1073
|
+
// type="file">`) is downscaled and JPEG-encoded here, which is both the size
|
|
1074
|
+
// cap the worker enforces and the trust boundary that keeps a non-image off
|
|
1075
|
+
// the wire. A string is assumed to be a prepared data URI and passes through.
|
|
1076
|
+
const searchImage = async (image, text, limit, signal) => {
|
|
1077
|
+
var _a;
|
|
1078
|
+
const dataUri = typeof image === 'string' ? image : await downscaleImage(image);
|
|
1079
|
+
logger.info(`EvoSearch API: Image search request - text length: ${(_a = text === null || text === void 0 ? void 0 : text.length) !== null && _a !== void 0 ? _a : 0}`);
|
|
1080
|
+
const headers = getHeaders();
|
|
1081
|
+
headers.set('Content-Type', 'application/json');
|
|
1082
|
+
// The widget and the hook wrap this in a no-retry resilience budget; a raw
|
|
1083
|
+
// consumer that passes no signal gets the same deadline here rather than a
|
|
1084
|
+
// request that can hang indefinitely.
|
|
1085
|
+
const controller = new AbortController();
|
|
1086
|
+
const onCallerAbort = () => controller.abort();
|
|
1087
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onCallerAbort);
|
|
1088
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
1089
|
+
controller.abort();
|
|
1090
|
+
}
|
|
1091
|
+
const timer = setTimeout(() => controller.abort(), IMAGE_SEARCH_TIMEOUT_MS);
|
|
1092
|
+
let response;
|
|
1093
|
+
try {
|
|
1094
|
+
response = await fetchImpl(getURL('/search/image'), {
|
|
1095
|
+
method: 'POST',
|
|
1096
|
+
headers,
|
|
1097
|
+
body: JSON.stringify({
|
|
1098
|
+
image: dataUri,
|
|
1099
|
+
...(text ? { text } : {}),
|
|
1100
|
+
...(limit === undefined ? {} : { limit }),
|
|
1101
|
+
}),
|
|
1102
|
+
credentials: 'omit',
|
|
1103
|
+
signal: controller.signal,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
finally {
|
|
1107
|
+
clearTimeout(timer);
|
|
1108
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onCallerAbort);
|
|
1109
|
+
}
|
|
1110
|
+
if (!response.ok) {
|
|
1111
|
+
const errorText = await response.text().catch(() => '');
|
|
1112
|
+
logger.error(`EvoSearch API: ImageSearch failed - ${response.status}: ${errorText}`);
|
|
1113
|
+
const error = new Error(`ImageSearch failed - ${response.status}`);
|
|
1114
|
+
error.status = response.status;
|
|
1115
|
+
if (response.status === 403 && isDisabledBody(errorText)) {
|
|
1116
|
+
error.disabled = true;
|
|
1117
|
+
}
|
|
1118
|
+
throw error;
|
|
1119
|
+
}
|
|
1120
|
+
return (await response.json());
|
|
1121
|
+
};
|
|
929
1122
|
// SSE consumer for Ask AI. Decodes `event: <type>\ndata: <json>` frames
|
|
930
1123
|
// from the worker stream and yields typed events as they arrive. Caller
|
|
931
1124
|
// aborts via the AbortSignal — fetch teardown closes the underlying
|
|
@@ -1091,10 +1284,19 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
1091
1284
|
clearRecents,
|
|
1092
1285
|
},
|
|
1093
1286
|
search,
|
|
1287
|
+
searchImage,
|
|
1094
1288
|
askStream,
|
|
1095
1289
|
sendEvent,
|
|
1096
1290
|
};
|
|
1097
1291
|
};
|
|
1292
|
+
const isDisabledBody = (body) => {
|
|
1293
|
+
try {
|
|
1294
|
+
return JSON.parse(body).disabled === true;
|
|
1295
|
+
}
|
|
1296
|
+
catch {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1098
1300
|
const parseSseFrame = (frame) => {
|
|
1099
1301
|
let eventType = '';
|
|
1100
1302
|
const dataLines = [];
|
|
@@ -1161,8 +1363,10 @@ const init = (params) => {
|
|
|
1161
1363
|
// A re-init can land on a different channel, or bail out entirely, so drop
|
|
1162
1364
|
// any previous cadence before deciding which one this run owns.
|
|
1163
1365
|
disposeKeepWarm === null || disposeKeepWarm === void 0 ? void 0 : disposeKeepWarm();
|
|
1164
|
-
|
|
1366
|
+
const evsParam = new URLSearchParams(window.location.search).get('evs');
|
|
1367
|
+
if (evsParam === 'preview') {
|
|
1165
1368
|
try {
|
|
1369
|
+
sessionStorage.removeItem(disableKeyName);
|
|
1166
1370
|
sessionStorage.setItem(previewKeyName, '1');
|
|
1167
1371
|
logger.info('EvoSearch SDK: Preview mode enabled via ?evs=preview URL param');
|
|
1168
1372
|
}
|
|
@@ -1170,11 +1374,32 @@ const init = (params) => {
|
|
|
1170
1374
|
error('EvoSearch SDK: Failed to set previewEvoSearch sessionStorage');
|
|
1171
1375
|
}
|
|
1172
1376
|
}
|
|
1377
|
+
else if (evsParam === 'off') {
|
|
1378
|
+
try {
|
|
1379
|
+
sessionStorage.removeItem(previewKeyName);
|
|
1380
|
+
sessionStorage.setItem(disableKeyName, '1');
|
|
1381
|
+
logger.info('EvoSearch SDK: Disabled via ?evs=off URL param');
|
|
1382
|
+
}
|
|
1383
|
+
catch {
|
|
1384
|
+
error('EvoSearch SDK: Failed to set disableEvoSearch sessionStorage');
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
else if (evsParam === 'reset') {
|
|
1388
|
+
try {
|
|
1389
|
+
sessionStorage.removeItem(previewKeyName);
|
|
1390
|
+
sessionStorage.removeItem(disableKeyName);
|
|
1391
|
+
logger.info('EvoSearch SDK: Reset to configured state via ?evs=reset URL param');
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
error('EvoSearch SDK: Failed to clear evoSearch sessionStorage');
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1173
1397
|
const { enabled, uiPreferences, askAi } = (_c = (_b = params.manifest) === null || _b === void 0 ? void 0 : _b.variables) !== null && _c !== void 0 ? _c : {};
|
|
1174
1398
|
const mode = uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.mode;
|
|
1175
1399
|
const askAiEnabled = (askAi === null || askAi === void 0 ? void 0 : askAi.enabled) === true;
|
|
1400
|
+
const visualSearchEnabled = (uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.visualSearch) === true;
|
|
1176
1401
|
const hasPreview = hasPreviewKey();
|
|
1177
|
-
const shouldEnable = enabled || hasPreview;
|
|
1402
|
+
const shouldEnable = !hasDisableKey() && (enabled || hasPreview);
|
|
1178
1403
|
if (!shouldEnable) {
|
|
1179
1404
|
logger.info('EvoSearch SDK: Mode is disabled, skipping initialization');
|
|
1180
1405
|
return;
|
|
@@ -1222,6 +1447,7 @@ const init = (params) => {
|
|
|
1222
1447
|
api: evoSearchAPI,
|
|
1223
1448
|
uiPreferences,
|
|
1224
1449
|
askAiEnabled,
|
|
1450
|
+
visualSearchEnabled,
|
|
1225
1451
|
});
|
|
1226
1452
|
params.sendTag({
|
|
1227
1453
|
eventId: crypto.randomUUID(),
|
|
@@ -1244,7 +1470,7 @@ const init = (params) => {
|
|
|
1244
1470
|
const tag = ({ data }) => {
|
|
1245
1471
|
try {
|
|
1246
1472
|
return {
|
|
1247
|
-
sdkVersion: "1.
|
|
1473
|
+
sdkVersion: "1.67.0" || '',
|
|
1248
1474
|
searchQuery: data['searchQuery'],
|
|
1249
1475
|
searchResults: data['searchResults'],
|
|
1250
1476
|
timestamp: Date.now(),
|
|
@@ -1253,7 +1479,7 @@ const tag = ({ data }) => {
|
|
|
1253
1479
|
catch (err) {
|
|
1254
1480
|
console.error(`EvoSearch SDK: Tagging failed - ${err}`);
|
|
1255
1481
|
return {
|
|
1256
|
-
sdkVersion: "1.
|
|
1482
|
+
sdkVersion: "1.67.0" ,
|
|
1257
1483
|
timestamp: Date.now(),
|
|
1258
1484
|
};
|
|
1259
1485
|
}
|